| // 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/gestures.dart'; |
| import 'package:flutter/material.dart'; |
| import 'package:flutter/rendering.dart'; |
| import 'package:flutter_test/flutter_test.dart'; |
| |
| import '../rendering/mock_canvas.dart'; |
| |
| class TestIcon extends StatefulWidget { |
| const TestIcon({ super.key }); |
| |
| @override |
| TestIconState createState() => TestIconState(); |
| } |
| |
| class TestIconState extends State<TestIcon> { |
| late IconThemeData iconTheme; |
| |
| @override |
| Widget build(BuildContext context) { |
| iconTheme = IconTheme.of(context); |
| return const Icon(Icons.add); |
| } |
| } |
| |
| class TestText extends StatefulWidget { |
| const TestText(this.text, { super.key }); |
| |
| final String text; |
| |
| @override |
| TestTextState createState() => TestTextState(); |
| } |
| |
| class TestTextState extends State<TestText> { |
| late TextStyle textStyle; |
| |
| @override |
| Widget build(BuildContext context) { |
| textStyle = DefaultTextStyle.of(context).style; |
| return Text(widget.text); |
| } |
| } |
| |
| void main() { |
| test('ListTileThemeData copyWith, ==, hashCode basics', () { |
| expect(const ListTileThemeData(), const ListTileThemeData().copyWith()); |
| expect(const ListTileThemeData().hashCode, const ListTileThemeData().copyWith().hashCode); |
| }); |
| |
| test('ListTileThemeData defaults', () { |
| const ListTileThemeData themeData = ListTileThemeData(); |
| expect(themeData.dense, null); |
| expect(themeData.shape, null); |
| expect(themeData.style, null); |
| expect(themeData.selectedColor, null); |
| expect(themeData.iconColor, null); |
| expect(themeData.textColor, null); |
| expect(themeData.contentPadding, null); |
| expect(themeData.tileColor, null); |
| expect(themeData.selectedTileColor, null); |
| expect(themeData.horizontalTitleGap, null); |
| expect(themeData.minVerticalPadding, null); |
| expect(themeData.minLeadingWidth, null); |
| expect(themeData.enableFeedback, null); |
| expect(themeData.mouseCursor, null); |
| expect(themeData.visualDensity, null); |
| }); |
| |
| testWidgets('Default ListTileThemeData debugFillProperties', (WidgetTester tester) async { |
| final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); |
| const ListTileThemeData().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('ListTileThemeData implements debugFillProperties', (WidgetTester tester) async { |
| final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); |
| const ListTileThemeData( |
| dense: true, |
| shape: StadiumBorder(), |
| style: ListTileStyle.drawer, |
| selectedColor: Color(0x00000001), |
| iconColor: Color(0x00000002), |
| textColor: Color(0x00000003), |
| contentPadding: EdgeInsets.all(100), |
| tileColor: Color(0x00000004), |
| selectedTileColor: Color(0x00000005), |
| horizontalTitleGap: 200, |
| minVerticalPadding: 300, |
| minLeadingWidth: 400, |
| enableFeedback: true, |
| mouseCursor: MaterialStateMouseCursor.clickable, |
| visualDensity: VisualDensity.comfortable, |
| ).debugFillProperties(builder); |
| |
| final List<String> description = builder.properties |
| .where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info)) |
| .map((DiagnosticsNode node) => node.toString()) |
| .toList(); |
| |
| expect(description[0], 'dense: true'); |
| expect(description[1], 'shape: StadiumBorder(BorderSide(Color(0xff000000), 0.0, BorderStyle.none))'); |
| expect(description[2], 'style: drawer'); |
| expect(description[3], 'selectedColor: Color(0x00000001)'); |
| expect(description[4], 'iconColor: Color(0x00000002)'); |
| expect(description[5], 'textColor: Color(0x00000003)'); |
| expect(description[6], 'contentPadding: EdgeInsets.all(100.0)'); |
| expect(description[7], 'tileColor: Color(0x00000004)'); |
| expect(description[8], 'selectedTileColor: Color(0x00000005)'); |
| expect(description[9], 'horizontalTitleGap: 200.0'); |
| expect(description[10], 'minVerticalPadding: 300.0'); |
| expect(description[11], 'minLeadingWidth: 400.0'); |
| expect(description[12], 'enableFeedback: true'); |
| expect(description[13], 'mouseCursor: MaterialStateMouseCursor(clickable)'); |
| expect( |
| description[14], |
| equalsIgnoringHashCodes('visualDensity: VisualDensity#00000(h: -1.0, v: -1.0)(horizontal: -1.0, vertical: -1.0)'), |
| ); |
| }); |
| |
| testWidgets('ListTileTheme backwards compatibility constructor', (WidgetTester tester) async { |
| late ListTileThemeData theme; |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: ListTileTheme( |
| dense: true, |
| shape: const StadiumBorder(), |
| style: ListTileStyle.drawer, |
| selectedColor: const Color(0x00000001), |
| iconColor: const Color(0x00000002), |
| textColor: const Color(0x00000003), |
| contentPadding: const EdgeInsets.all(100), |
| tileColor: const Color(0x00000004), |
| selectedTileColor: const Color(0x00000005), |
| horizontalTitleGap: 200, |
| minVerticalPadding: 300, |
| minLeadingWidth: 400, |
| enableFeedback: true, |
| mouseCursor: MaterialStateMouseCursor.clickable, |
| child: Center( |
| child: Builder( |
| builder: (BuildContext context) { |
| theme = ListTileTheme.of(context); |
| return const Placeholder(); |
| }, |
| ), |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| expect(theme.dense, true); |
| expect(theme.shape, const StadiumBorder()); |
| expect(theme.style, ListTileStyle.drawer); |
| expect(theme.selectedColor, const Color(0x00000001)); |
| expect(theme.iconColor, const Color(0x00000002)); |
| expect(theme.textColor, const Color(0x00000003)); |
| expect(theme.contentPadding, const EdgeInsets.all(100)); |
| expect(theme.tileColor, const Color(0x00000004)); |
| expect(theme.selectedTileColor, const Color(0x00000005)); |
| expect(theme.horizontalTitleGap, 200); |
| expect(theme.minVerticalPadding, 300); |
| expect(theme.minLeadingWidth, 400); |
| expect(theme.enableFeedback, true); |
| expect(theme.mouseCursor, MaterialStateMouseCursor.clickable); |
| }); |
| |
| testWidgets('ListTileTheme', (WidgetTester tester) async { |
| final Key listTileKey = UniqueKey(); |
| final Key titleKey = UniqueKey(); |
| final Key subtitleKey = UniqueKey(); |
| final Key leadingKey = UniqueKey(); |
| final Key trailingKey = UniqueKey(); |
| late ThemeData theme; |
| |
| Widget buildFrame({ |
| bool enabled = true, |
| bool dense = false, |
| bool selected = false, |
| ShapeBorder? shape, |
| Color? selectedColor, |
| Color? iconColor, |
| Color? textColor, |
| }) { |
| return MaterialApp( |
| home: Material( |
| child: Center( |
| child: ListTileTheme( |
| data: ListTileThemeData( |
| dense: dense, |
| shape: shape, |
| selectedColor: selectedColor, |
| iconColor: iconColor, |
| textColor: textColor, |
| mouseCursor: MaterialStateProperty.resolveWith((Set<MaterialState> states) { |
| if (states.contains(MaterialState.disabled)) { |
| return SystemMouseCursors.forbidden; |
| } |
| |
| return SystemMouseCursors.click; |
| }), |
| visualDensity: VisualDensity.compact, |
| ), |
| child: Builder( |
| builder: (BuildContext context) { |
| theme = Theme.of(context); |
| return ListTile( |
| key: listTileKey, |
| enabled: enabled, |
| selected: selected, |
| leading: TestIcon(key: leadingKey), |
| trailing: TestIcon(key: trailingKey), |
| title: TestText('title', key: titleKey), |
| subtitle: TestText('subtitle', key: subtitleKey), |
| ); |
| }, |
| ), |
| ), |
| ), |
| ), |
| ); |
| } |
| |
| const Color green = Color(0xFF00FF00); |
| const Color red = Color(0xFFFF0000); |
| const ShapeBorder roundedShape = RoundedRectangleBorder( |
| borderRadius: BorderRadius.all(Radius.circular(4.0)), |
| ); |
| |
| Color iconColor(Key key) => tester.state<TestIconState>(find.byKey(key)).iconTheme.color!; |
| Color textColor(Key key) => tester.state<TestTextState>(find.byKey(key)).textStyle.color!; |
| ShapeBorder inkWellBorder() => tester.widget<InkWell>(find.descendant(of: find.byType(ListTile), matching: find.byType(InkWell))).customBorder!; |
| |
| // A selected ListTile's leading, trailing, and text get the primary color by default |
| await tester.pumpWidget(buildFrame(selected: true)); |
| await tester.pump(const Duration(milliseconds: 300)); // DefaultTextStyle changes animate |
| expect(iconColor(leadingKey), theme.primaryColor); |
| expect(iconColor(trailingKey), theme.primaryColor); |
| expect(textColor(titleKey), theme.primaryColor); |
| expect(textColor(subtitleKey), theme.primaryColor); |
| |
| // A selected ListTile's leading, trailing, and text get the ListTileTheme's selectedColor |
| await tester.pumpWidget(buildFrame(selected: true, selectedColor: green)); |
| await tester.pump(const Duration(milliseconds: 300)); // DefaultTextStyle changes animate |
| expect(iconColor(leadingKey), green); |
| expect(iconColor(trailingKey), green); |
| expect(textColor(titleKey), green); |
| expect(textColor(subtitleKey), green); |
| |
| // An unselected ListTile's leading and trailing get the ListTileTheme's iconColor |
| // An unselected ListTile's title texts get the ListTileTheme's textColor |
| await tester.pumpWidget(buildFrame(iconColor: red, textColor: green)); |
| await tester.pump(const Duration(milliseconds: 300)); // DefaultTextStyle changes animate |
| expect(iconColor(leadingKey), red); |
| expect(iconColor(trailingKey), red); |
| expect(textColor(titleKey), green); |
| expect(textColor(subtitleKey), green); |
| |
| // If the item is disabled it's rendered with the theme's disabled color. |
| await tester.pumpWidget(buildFrame(enabled: false)); |
| await tester.pump(const Duration(milliseconds: 300)); // DefaultTextStyle changes animate |
| expect(iconColor(leadingKey), theme.disabledColor); |
| expect(iconColor(trailingKey), theme.disabledColor); |
| expect(textColor(titleKey), theme.disabledColor); |
| expect(textColor(subtitleKey), theme.disabledColor); |
| |
| // If the item is disabled it's rendered with the theme's disabled color. |
| // Even if it's selected. |
| await tester.pumpWidget(buildFrame(enabled: false, selected: true)); |
| await tester.pump(const Duration(milliseconds: 300)); // DefaultTextStyle changes animate |
| expect(iconColor(leadingKey), theme.disabledColor); |
| expect(iconColor(trailingKey), theme.disabledColor); |
| expect(textColor(titleKey), theme.disabledColor); |
| expect(textColor(subtitleKey), theme.disabledColor); |
| |
| // A selected ListTile's InkWell gets the ListTileTheme's shape |
| await tester.pumpWidget(buildFrame(selected: true, shape: roundedShape)); |
| expect(inkWellBorder(), roundedShape); |
| |
| // Cursor updates when hovering disabled ListTile |
| await tester.pumpWidget(buildFrame(enabled: false)); |
| final Offset listTile = tester.getCenter(find.byKey(titleKey)); |
| final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); |
| await gesture.addPointer(); |
| await gesture.moveTo(listTile); |
| await tester.pumpAndSettle(); |
| expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.forbidden); |
| |
| // VisualDensity is respected |
| final RenderBox box = tester.renderObject(find.byKey(listTileKey)); |
| expect(box.size, equals(const Size(800, 64.0))); |
| }); |
| |
| testWidgets('ListTileTheme colors are applied to leading and trailing text widgets', (WidgetTester tester) async { |
| final Key leadingKey = UniqueKey(); |
| final Key trailingKey = UniqueKey(); |
| |
| const Color selectedColor = Colors.orange; |
| const Color defaultColor = Colors.black; |
| |
| late ThemeData theme; |
| Widget buildFrame({ |
| bool enabled = true, |
| bool selected = false, |
| }) { |
| return MaterialApp( |
| home: Material( |
| child: Center( |
| child: ListTileTheme( |
| data: const ListTileThemeData( |
| selectedColor: selectedColor, |
| textColor: defaultColor, |
| ), |
| child: Builder( |
| builder: (BuildContext context) { |
| theme = Theme.of(context); |
| return ListTile( |
| enabled: enabled, |
| selected: selected, |
| leading: TestText('leading', key: leadingKey), |
| title: const TestText('title'), |
| trailing: TestText('trailing', key: trailingKey), |
| ); |
| }, |
| ), |
| ), |
| ), |
| ), |
| ); |
| } |
| |
| Color textColor(Key key) => tester.state<TestTextState>(find.byKey(key)).textStyle.color!; |
| |
| await tester.pumpWidget(buildFrame()); |
| // Enabled color should use ListTileTheme.textColor. |
| expect(textColor(leadingKey), defaultColor); |
| expect(textColor(trailingKey), defaultColor); |
| |
| await tester.pumpWidget(buildFrame(selected: true)); |
| // Wait for text color to animate. |
| await tester.pumpAndSettle(); |
| // Selected color should use ListTileTheme.selectedColor. |
| expect(textColor(leadingKey), selectedColor); |
| expect(textColor(trailingKey), selectedColor); |
| |
| await tester.pumpWidget(buildFrame(enabled: false)); |
| // Wait for text color to animate. |
| await tester.pumpAndSettle(); |
| // Disabled color should be ThemeData.disabledColor. |
| expect(textColor(leadingKey), theme.disabledColor); |
| expect(textColor(trailingKey), theme.disabledColor); |
| }); |
| |
| testWidgets("ListTile respects ListTileTheme's tileColor & selectedTileColor", (WidgetTester tester) async { |
| late ListTileThemeData theme; |
| bool isSelected = false; |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: ListTileTheme( |
| data: ListTileThemeData( |
| tileColor: Colors.green.shade500, |
| selectedTileColor: Colors.red.shade500, |
| ), |
| child: Center( |
| child: StatefulBuilder( |
| builder: (BuildContext context, StateSetter setState) { |
| theme = ListTileTheme.of(context); |
| return ListTile( |
| selected: isSelected, |
| onTap: () { |
| setState(()=> isSelected = !isSelected); |
| }, |
| title: const Text('Title'), |
| ); |
| }, |
| ), |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| expect(find.byType(Material), paints..path(color: theme.tileColor)); |
| |
| // Tap on tile to change isSelected. |
| await tester.tap(find.byType(ListTile)); |
| await tester.pumpAndSettle(); |
| |
| expect(find.byType(Material), paints..path(color: theme.selectedTileColor)); |
| }); |
| |
| testWidgets("ListTileTheme's tileColor & selectedTileColor are overridden by ListTile properties", (WidgetTester tester) async { |
| bool isSelected = false; |
| final Color tileColor = Colors.green.shade500; |
| final Color selectedTileColor = Colors.red.shade500; |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: ListTileTheme( |
| data: const ListTileThemeData( |
| selectedTileColor: Colors.green, |
| tileColor: Colors.red, |
| ), |
| child: Center( |
| child: StatefulBuilder( |
| builder: (BuildContext context, StateSetter setState) { |
| return ListTile( |
| tileColor: tileColor, |
| selectedTileColor: selectedTileColor, |
| selected: isSelected, |
| onTap: () { |
| setState(()=> isSelected = !isSelected); |
| }, |
| title: const Text('Title'), |
| ); |
| }, |
| ), |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| expect(find.byType(Material), paints..path(color: tileColor)); |
| |
| // Tap on tile to change isSelected. |
| await tester.tap(find.byType(ListTile)); |
| await tester.pumpAndSettle(); |
| |
| expect(find.byType(Material), paints..path(color: selectedTileColor)); |
| }); |
| } |