| // 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 '../foundation/leak_tracking.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 lerp special cases', () { |
| expect(ListTileThemeData.lerp(null, null, 0), null); |
| const ListTileThemeData data = ListTileThemeData(); |
| expect(identical(ListTileThemeData.lerp(data, data, 0.5), data), true); |
| }); |
| |
| 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.titleTextStyle, null); |
| expect(themeData.subtitleTextStyle, null); |
| expect(themeData.leadingAndTrailingTextStyle, 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); |
| expect(themeData.titleAlignment, null); |
| }); |
| |
| testWidgetsWithLeakTracking('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>[]); |
| }); |
| |
| testWidgetsWithLeakTracking('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), |
| titleTextStyle: TextStyle(color: Color(0x00000004)), |
| subtitleTextStyle: TextStyle(color: Color(0x00000005)), |
| leadingAndTrailingTextStyle: TextStyle(color: Color(0x00000006)), |
| contentPadding: EdgeInsets.all(100), |
| tileColor: Color(0x00000007), |
| selectedTileColor: Color(0x00000008), |
| horizontalTitleGap: 200, |
| minVerticalPadding: 300, |
| minLeadingWidth: 400, |
| enableFeedback: true, |
| mouseCursor: MaterialStateMouseCursor.clickable, |
| visualDensity: VisualDensity.comfortable, |
| titleAlignment: ListTileTitleAlignment.top, |
| ).debugFillProperties(builder); |
| |
| final List<String> description = builder.properties |
| .where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info)) |
| .map((DiagnosticsNode node) => node.toString()) |
| .toList(); |
| |
| expect( |
| description, |
| equalsIgnoringHashCodes(<String>[ |
| 'dense: true', |
| 'shape: StadiumBorder(BorderSide(width: 0.0, style: none))', |
| 'style: drawer', |
| 'selectedColor: Color(0x00000001)', |
| 'iconColor: Color(0x00000002)', |
| 'textColor: Color(0x00000003)', |
| 'titleTextStyle: TextStyle(inherit: true, color: Color(0x00000004))', |
| 'subtitleTextStyle: TextStyle(inherit: true, color: Color(0x00000005))', |
| 'leadingAndTrailingTextStyle: TextStyle(inherit: true, color: Color(0x00000006))', |
| 'contentPadding: EdgeInsets.all(100.0)', |
| 'tileColor: Color(0x00000007)', |
| 'selectedTileColor: Color(0x00000008)', |
| 'horizontalTitleGap: 200.0', |
| 'minVerticalPadding: 300.0', |
| 'minLeadingWidth: 400.0', |
| 'enableFeedback: true', |
| 'mouseCursor: MaterialStateMouseCursor(clickable)', |
| 'visualDensity: VisualDensity#00000(h: -1.0, v: -1.0)(horizontal: -1.0, vertical: -1.0)', |
| 'titleAlignment: ListTileTitleAlignment.top', |
| ]), |
| ); |
| }); |
| |
| testWidgetsWithLeakTracking('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); |
| }); |
| |
| testWidgetsWithLeakTracking('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( |
| theme: ThemeData(useMaterial3: false), |
| home: Material( |
| child: Center( |
| child: ListTileTheme( |
| data: ListTileThemeData( |
| dense: dense, |
| shape: shape, |
| selectedColor: selectedColor, |
| iconColor: iconColor, |
| textColor: textColor, |
| minVerticalPadding: 25.0, |
| mouseCursor: MaterialStateProperty.resolveWith((Set<MaterialState> states) { |
| if (states.contains(MaterialState.disabled)) { |
| return SystemMouseCursors.forbidden; |
| } |
| |
| return SystemMouseCursors.click; |
| }), |
| visualDensity: VisualDensity.compact, |
| titleAlignment: ListTileTitleAlignment.bottom, |
| ), |
| 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, 80.0))); |
| |
| // titleAlignment is respected. |
| final Offset titleOffset = tester.getTopLeft(find.text('title')); |
| final Offset leadingOffset = tester.getTopLeft(find.byKey(leadingKey)); |
| final Offset trailingOffset = tester.getTopRight(find.byKey(trailingKey)); |
| expect(leadingOffset.dy - titleOffset.dy, 6); |
| expect(trailingOffset.dy - titleOffset.dy, 6); |
| }); |
| |
| testWidgetsWithLeakTracking('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); |
| }); |
| |
| testWidgetsWithLeakTracking( |
| "ListTile respects ListTileTheme's titleTextStyle, subtitleTextStyle & leadingAndTrailingTextStyle", |
| (WidgetTester tester) async { |
| final ThemeData theme = ThemeData( |
| useMaterial3: true, |
| listTileTheme: const ListTileThemeData( |
| titleTextStyle: TextStyle(fontSize: 20.0), |
| subtitleTextStyle: TextStyle(fontSize: 17.5), |
| leadingAndTrailingTextStyle: TextStyle(fontSize: 15.0), |
| ), |
| ); |
| |
| Widget buildFrame() { |
| return MaterialApp( |
| theme: theme, |
| home: Material( |
| child: Center( |
| child: Builder( |
| builder: (BuildContext context) { |
| return const ListTile( |
| leading: TestText('leading'), |
| title: TestText('title'), |
| subtitle: TestText('subtitle'), |
| trailing: TestText('trailing'), |
| ); |
| }, |
| ), |
| ), |
| ), |
| ); |
| } |
| |
| await tester.pumpWidget(buildFrame()); |
| final RenderParagraph leading = _getTextRenderObject(tester, 'leading'); |
| expect(leading.text.style!.fontSize, 15.0); |
| final RenderParagraph title = _getTextRenderObject(tester, 'title'); |
| expect(title.text.style!.fontSize, 20.0); |
| final RenderParagraph subtitle = _getTextRenderObject(tester, 'subtitle'); |
| expect(subtitle.text.style!.fontSize, 17.5); |
| final RenderParagraph trailing = _getTextRenderObject(tester, 'trailing'); |
| expect(trailing.text.style!.fontSize, 15.0); |
| }); |
| |
| testWidgetsWithLeakTracking( |
| "ListTile's titleTextStyle, subtitleTextStyle & leadingAndTrailingTextStyle are overridden by ListTile properties", |
| (WidgetTester tester) async { |
| final ThemeData theme = ThemeData( |
| useMaterial3: true, |
| listTileTheme: const ListTileThemeData( |
| titleTextStyle: TextStyle(fontSize: 20.0), |
| subtitleTextStyle: TextStyle(fontSize: 17.5), |
| leadingAndTrailingTextStyle: TextStyle(fontSize: 15.0), |
| ), |
| ); |
| |
| const TextStyle titleTextStyle = TextStyle(fontSize: 23.0); |
| const TextStyle subtitleTextStyle = TextStyle(fontSize: 20.0); |
| const TextStyle leadingAndTrailingTextStyle = TextStyle(fontSize: 18.0); |
| |
| Widget buildFrame() { |
| return MaterialApp( |
| theme: theme, |
| home: Material( |
| child: Center( |
| child: Builder( |
| builder: (BuildContext context) { |
| return const ListTile( |
| titleTextStyle: titleTextStyle, |
| subtitleTextStyle: subtitleTextStyle, |
| leadingAndTrailingTextStyle: leadingAndTrailingTextStyle, |
| leading: TestText('leading'), |
| title: TestText('title'), |
| subtitle: TestText('subtitle'), |
| trailing: TestText('trailing'), |
| ); |
| }, |
| ), |
| ), |
| ), |
| ); |
| } |
| |
| await tester.pumpWidget(buildFrame()); |
| final RenderParagraph leading = _getTextRenderObject(tester, 'leading'); |
| expect(leading.text.style!.fontSize, 18.0); |
| final RenderParagraph title = _getTextRenderObject(tester, 'title'); |
| expect(title.text.style!.fontSize, 23.0); |
| final RenderParagraph subtitle = _getTextRenderObject(tester, 'subtitle'); |
| expect(subtitle.text.style!.fontSize, 20.0); |
| final RenderParagraph trailing = _getTextRenderObject(tester, 'trailing'); |
| expect(trailing.text.style!.fontSize, 18.0); |
| }); |
| |
| testWidgetsWithLeakTracking("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..rect(color: theme.tileColor)); |
| |
| // Tap on tile to change isSelected. |
| await tester.tap(find.byType(ListTile)); |
| await tester.pumpAndSettle(); |
| |
| expect(find.byType(Material), paints..rect(color: theme.selectedTileColor)); |
| }); |
| |
| testWidgetsWithLeakTracking("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..rect(color: tileColor)); |
| |
| // Tap on tile to change isSelected. |
| await tester.tap(find.byType(ListTile)); |
| await tester.pumpAndSettle(); |
| |
| expect(find.byType(Material), paints..rect(color: selectedTileColor)); |
| }); |
| |
| testWidgetsWithLeakTracking('ListTile uses ListTileTheme shape in a drawer', (WidgetTester tester) async { |
| // This is a regression test for https://github.com/flutter/flutter/issues/106303 |
| |
| final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>(); |
| final ShapeBorder shapeBorder = RoundedRectangleBorder(borderRadius: BorderRadius.circular(20.0)); |
| |
| await tester.pumpWidget(MaterialApp( |
| theme: ThemeData( |
| listTileTheme: ListTileThemeData(shape: shapeBorder), |
| ), |
| home: Scaffold( |
| key: scaffoldKey, |
| drawer: const Drawer( |
| child: ListTile(), |
| ), |
| body: Container(), |
| ), |
| )); |
| await tester.pumpAndSettle(); |
| |
| scaffoldKey.currentState!.openDrawer(); |
| // Start drawer animation. |
| await tester.pump(); |
| |
| final ShapeBorder? inkWellBorder = tester.widget<InkWell>( |
| find.descendant( |
| of: find.byType(ListTile), |
| matching: find.byType(InkWell), |
| )).customBorder; |
| // Test shape. |
| expect(inkWellBorder, shapeBorder); |
| }); |
| |
| testWidgetsWithLeakTracking('ListTile respects MaterialStateColor LisTileTheme.textColor', (WidgetTester tester) async { |
| bool enabled = false; |
| bool selected = false; |
| const Color defaultColor = Colors.blue; |
| const Color selectedColor = Colors.green; |
| const Color disabledColor = Colors.red; |
| |
| final ThemeData theme = ThemeData( |
| listTileTheme: ListTileThemeData( |
| textColor: MaterialStateColor.resolveWith((Set<MaterialState> states) { |
| if (states.contains(MaterialState.disabled)) { |
| return disabledColor; |
| } |
| if (states.contains(MaterialState.selected)) { |
| return selectedColor; |
| } |
| return defaultColor; |
| }), |
| ), |
| ); |
| Widget buildFrame() { |
| return MaterialApp( |
| theme: theme, |
| home: Material( |
| child: Center( |
| child: Builder( |
| builder: (BuildContext context) { |
| return ListTile( |
| enabled: enabled, |
| selected: selected, |
| title: const TestText('title'), |
| subtitle: const TestText('subtitle') , |
| ); |
| }, |
| ), |
| ), |
| ), |
| ); |
| } |
| |
| // Test disabled state. |
| await tester.pumpWidget(buildFrame()); |
| RenderParagraph title = _getTextRenderObject(tester, 'title'); |
| expect(title.text.style!.color, disabledColor); |
| |
| // Test enabled state. |
| enabled = true; |
| await tester.pumpWidget(buildFrame()); |
| await tester.pumpAndSettle(); |
| title = _getTextRenderObject(tester, 'title'); |
| expect(title.text.style!.color, defaultColor); |
| |
| // Test selected state. |
| selected = true; |
| await tester.pumpWidget(buildFrame()); |
| await tester.pumpAndSettle(); |
| title = _getTextRenderObject(tester, 'title'); |
| expect(title.text.style!.color, selectedColor); |
| }); |
| |
| testWidgetsWithLeakTracking('ListTile respects MaterialStateColor LisTileTheme.iconColor', (WidgetTester tester) async { |
| bool enabled = false; |
| bool selected = false; |
| const Color defaultColor = Colors.blue; |
| const Color selectedColor = Colors.green; |
| const Color disabledColor = Colors.red; |
| final Key leadingKey = UniqueKey(); |
| |
| final ThemeData theme = ThemeData( |
| listTileTheme: ListTileThemeData( |
| iconColor: MaterialStateColor.resolveWith((Set<MaterialState> states) { |
| if (states.contains(MaterialState.disabled)) { |
| return disabledColor; |
| } |
| if (states.contains(MaterialState.selected)) { |
| return selectedColor; |
| } |
| return defaultColor; |
| }), |
| ), |
| ); |
| Widget buildFrame() { |
| return MaterialApp( |
| theme: theme, |
| home: Material( |
| child: Center( |
| child: Builder( |
| builder: (BuildContext context) { |
| return ListTile( |
| enabled: enabled, |
| selected: selected, |
| leading: TestIcon(key: leadingKey), |
| ); |
| }, |
| ), |
| ), |
| ), |
| ); |
| } |
| |
| Color iconColor(Key key) => tester.state<TestIconState>(find.byKey(key)).iconTheme.color!; |
| |
| // Test disabled state. |
| await tester.pumpWidget(buildFrame()); |
| expect(iconColor(leadingKey), disabledColor); |
| |
| // Test enabled state. |
| enabled = true; |
| await tester.pumpWidget(buildFrame()); |
| await tester.pumpAndSettle(); |
| expect(iconColor(leadingKey), defaultColor); |
| |
| // Test selected state. |
| selected = true; |
| await tester.pumpWidget(buildFrame()); |
| await tester.pumpAndSettle(); |
| expect(iconColor(leadingKey), selectedColor); |
| }); |
| |
| testWidgetsWithLeakTracking('ListTileThemeData copyWith overrides all properties', (WidgetTester tester) async { |
| // This is a regression test for https://github.com/flutter/flutter/issues/119734 |
| |
| const ListTileThemeData original = ListTileThemeData( |
| dense: true, |
| shape: StadiumBorder(), |
| style: ListTileStyle.drawer, |
| selectedColor: Color(0x00000001), |
| iconColor: Color(0x00000002), |
| textColor: Color(0x00000003), |
| titleTextStyle: TextStyle(color: Color(0x00000004)), |
| subtitleTextStyle: TextStyle(color: Color(0x00000005)), |
| leadingAndTrailingTextStyle: TextStyle(color: Color(0x00000006)), |
| contentPadding: EdgeInsets.all(100), |
| tileColor: Color(0x00000007), |
| selectedTileColor: Color(0x00000008), |
| horizontalTitleGap: 200, |
| minVerticalPadding: 300, |
| minLeadingWidth: 400, |
| enableFeedback: true, |
| titleAlignment: ListTileTitleAlignment.bottom, |
| ); |
| |
| final ListTileThemeData copy = original.copyWith( |
| dense: false, |
| shape: const RoundedRectangleBorder(), |
| style: ListTileStyle.list, |
| selectedColor: const Color(0x00000009), |
| iconColor: const Color(0x0000000A), |
| textColor: const Color(0x0000000B), |
| titleTextStyle: const TextStyle(color: Color(0x0000000C)), |
| subtitleTextStyle: const TextStyle(color: Color(0x0000000D)), |
| leadingAndTrailingTextStyle: const TextStyle(color: Color(0x0000000E)), |
| contentPadding: const EdgeInsets.all(500), |
| tileColor: const Color(0x0000000F), |
| selectedTileColor: const Color(0x00000010), |
| horizontalTitleGap: 600, |
| minVerticalPadding: 700, |
| minLeadingWidth: 800, |
| enableFeedback: false, |
| titleAlignment: ListTileTitleAlignment.top, |
| ); |
| |
| expect(copy.dense, false); |
| expect(copy.shape, const RoundedRectangleBorder()); |
| expect(copy.style, ListTileStyle.list); |
| expect(copy.selectedColor, const Color(0x00000009)); |
| expect(copy.iconColor, const Color(0x0000000A)); |
| expect(copy.textColor, const Color(0x0000000B)); |
| expect(copy.titleTextStyle, const TextStyle(color: Color(0x0000000C))); |
| expect(copy.subtitleTextStyle, const TextStyle(color: Color(0x0000000D))); |
| expect(copy.leadingAndTrailingTextStyle, const TextStyle(color: Color(0x0000000E))); |
| expect(copy.contentPadding, const EdgeInsets.all(500)); |
| expect(copy.tileColor, const Color(0x0000000F)); |
| expect(copy.selectedTileColor, const Color(0x00000010)); |
| expect(copy.horizontalTitleGap, 600); |
| expect(copy.minVerticalPadding, 700); |
| expect(copy.minLeadingWidth, 800); |
| expect(copy.enableFeedback, false); |
| expect(copy.titleAlignment, ListTileTitleAlignment.top); |
| }); |
| |
| testWidgetsWithLeakTracking('ListTileTheme.titleAlignment is overridden by ListTile.titleAlignment', (WidgetTester tester) async { |
| final Key leadingKey = GlobalKey(); |
| final Key trailingKey = GlobalKey(); |
| const String titleText = '\nHeadline Text\n'; |
| const String subtitleText = '\nSupporting Text\n'; |
| |
| Widget buildFrame({ ListTileTitleAlignment? alignment }) { |
| return MaterialApp( |
| theme: ThemeData( |
| useMaterial3: true, |
| listTileTheme: const ListTileThemeData( |
| titleAlignment: ListTileTitleAlignment.center, |
| ), |
| ), |
| home: Material( |
| child: Center( |
| child: ListTile( |
| titleAlignment: ListTileTitleAlignment.top, |
| leading: SizedBox(key: leadingKey, width: 24.0, height: 24.0), |
| title: const Text(titleText), |
| subtitle: const Text(subtitleText), |
| trailing: SizedBox(key: trailingKey, width: 24.0, height: 24.0), |
| ), |
| ), |
| ), |
| ); |
| } |
| |
| await tester.pumpWidget(buildFrame()); |
| final Offset tileOffset = tester.getTopLeft(find.byType(ListTile)); |
| final Offset leadingOffset = tester.getTopLeft(find.byKey(leadingKey)); |
| final Offset trailingOffset = tester.getTopRight(find.byKey(trailingKey)); |
| expect(leadingOffset.dy - tileOffset.dy, 8.0); |
| expect(trailingOffset.dy - tileOffset.dy, 8.0); |
| }); |
| |
| testWidgetsWithLeakTracking('ListTileTheme.merge supports all properties', (WidgetTester tester) async { |
| Widget buildFrame() { |
| return MaterialApp( |
| theme: ThemeData( |
| listTileTheme: const ListTileThemeData( |
| dense: true, |
| shape: StadiumBorder(), |
| style: ListTileStyle.drawer, |
| selectedColor: Color(0x00000001), |
| iconColor: Color(0x00000002), |
| textColor: Color(0x00000003), |
| titleTextStyle: TextStyle(color: Color(0x00000004)), |
| subtitleTextStyle: TextStyle(color: Color(0x00000005)), |
| leadingAndTrailingTextStyle: TextStyle(color: Color(0x00000006)), |
| contentPadding: EdgeInsets.all(100), |
| tileColor: Color(0x00000007), |
| selectedTileColor: Color(0x00000008), |
| horizontalTitleGap: 200, |
| minVerticalPadding: 300, |
| minLeadingWidth: 400, |
| enableFeedback: true, |
| titleAlignment: ListTileTitleAlignment.bottom, |
| mouseCursor: MaterialStateMouseCursor.textable, |
| visualDensity: VisualDensity.comfortable, |
| ), |
| ), |
| home: Material( |
| child: Center( |
| child: Builder( |
| builder: (BuildContext context) { |
| return ListTileTheme.merge( |
| dense: false, |
| shape: const RoundedRectangleBorder(), |
| style: ListTileStyle.list, |
| selectedColor: const Color(0x00000009), |
| iconColor: const Color(0x0000000A), |
| textColor: const Color(0x0000000B), |
| titleTextStyle: const TextStyle(color: Color(0x0000000C)), |
| subtitleTextStyle: const TextStyle(color: Color(0x0000000D)), |
| leadingAndTrailingTextStyle: const TextStyle(color: Color(0x0000000E)), |
| contentPadding: const EdgeInsets.all(500), |
| tileColor: const Color(0x0000000F), |
| selectedTileColor: const Color(0x00000010), |
| horizontalTitleGap: 600, |
| minVerticalPadding: 700, |
| minLeadingWidth: 800, |
| enableFeedback: false, |
| titleAlignment: ListTileTitleAlignment.top, |
| mouseCursor: MaterialStateMouseCursor.clickable, |
| visualDensity: VisualDensity.compact, |
| child: const ListTile(), |
| ); |
| } |
| ), |
| ), |
| ), |
| ); |
| } |
| |
| await tester.pumpWidget(buildFrame()); |
| final ListTileThemeData theme = ListTileTheme.of(tester.element(find.byType(ListTile))); |
| expect(theme.dense, false); |
| expect(theme.shape, const RoundedRectangleBorder()); |
| expect(theme.style, ListTileStyle.list); |
| expect(theme.selectedColor, const Color(0x00000009)); |
| expect(theme.iconColor, const Color(0x0000000A)); |
| expect(theme.textColor, const Color(0x0000000B)); |
| expect(theme.titleTextStyle, const TextStyle(color: Color(0x0000000C))); |
| expect(theme.subtitleTextStyle, const TextStyle(color: Color(0x0000000D))); |
| expect(theme.leadingAndTrailingTextStyle, const TextStyle(color: Color(0x0000000E))); |
| expect(theme.contentPadding, const EdgeInsets.all(500)); |
| expect(theme.tileColor, const Color(0x0000000F)); |
| expect(theme.selectedTileColor, const Color(0x00000010)); |
| expect(theme.horizontalTitleGap, 600); |
| expect(theme.minVerticalPadding, 700); |
| expect(theme.minLeadingWidth, 800); |
| expect(theme.enableFeedback, false); |
| expect(theme.titleAlignment, ListTileTitleAlignment.top); |
| expect(theme.mouseCursor, MaterialStateMouseCursor.clickable); |
| expect(theme.visualDensity, VisualDensity.compact); |
| }); |
| } |
| |
| RenderParagraph _getTextRenderObject(WidgetTester tester, String text) { |
| return tester.renderObject(find.descendant( |
| of: find.byType(ListTile), |
| matching: find.text(text), |
| )); |
| } |