// 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({ Key? key }) : super(key: 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, { Key? key }) : super(key: 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(); addTearDown(gesture.removePointer); 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)); }); }