From 0ef46388227174bd5cc7305301fc95dccde1df19 Mon Sep 17 00:00:00 2001 From: Taha Tesser Date: Mon, 22 Jan 2024 12:01:05 +0200 Subject: [PATCH] Update `ToggleButtons`, `ExpansionPanel`, and `ExpandIcon` tests for Material 3 (#141868) Updated unit tests for `ToggleButtons`, `ExpansionPanel`, and `ExpandIcon` to have M2 and M3 versions. More info in #139076 --- .../test/material/expand_icon_test.dart | 86 +++++++- .../test/material/expansion_panel_test.dart | 149 ++++++++++++- .../test/material/toggle_buttons_test.dart | 206 ++++++++++++++++-- 3 files changed, 419 insertions(+), 22 deletions(-) diff --git a/packages/flutter/test/material/expand_icon_test.dart b/packages/flutter/test/material/expand_icon_test.dart index 4d795da9adb6..7ec1338fa447 100644 --- a/packages/flutter/test/material/expand_icon_test.dart +++ b/packages/flutter/test/material/expand_icon_test.dart @@ -73,9 +73,9 @@ void main() { expect(iconTheme.data.color, equals(Colors.white60)); }); - testWidgets('ExpandIcon disabled', (WidgetTester tester) async { + testWidgets('Material2 - ExpandIcon disabled', (WidgetTester tester) async { IconTheme iconTheme; - // Light mode test + // Test light mode. await tester.pumpWidget(wrap( theme: ThemeData(useMaterial3: false), child: const ExpandIcon(onPressed: null), @@ -85,7 +85,7 @@ void main() { iconTheme = tester.firstWidget(find.byType(IconTheme).last); expect(iconTheme.data.color, equals(Colors.black38)); - // Dark mode test + // Test dark mode. await tester.pumpWidget(wrap( child: const ExpandIcon(onPressed: null), theme: ThemeData(useMaterial3: false, brightness: Brightness.dark), @@ -96,6 +96,37 @@ void main() { expect(iconTheme.data.color, equals(Colors.white38)); }); + testWidgets('Material3 - ExpandIcon disabled', (WidgetTester tester) async { + ThemeData theme = ThemeData(); + IconTheme iconTheme; + // Test light mode. + await tester.pumpWidget(wrap( + theme: theme, + child: const ExpandIcon(onPressed: null), + )); + await tester.pumpAndSettle(); + + iconTheme = tester.firstWidget(find.byType(IconTheme).last); + expect( + iconTheme.data.color, + equals(theme.colorScheme.onSurface.withOpacity(0.38)), + ); + + theme = ThemeData(brightness: Brightness.dark); + // Test dark mode. + await tester.pumpWidget(wrap( + theme: theme, + child: const ExpandIcon(onPressed: null), + )); + await tester.pumpAndSettle(); + + iconTheme = tester.firstWidget(find.byType(IconTheme).last); + expect( + iconTheme.data.color, + equals(theme.colorScheme.onSurface.withOpacity(0.38)), + ); + }); + testWidgets('ExpandIcon test isExpanded does not trigger callback', (WidgetTester tester) async { bool expanded = false; @@ -173,7 +204,7 @@ void main() { expect(icon.size, 48); }); - testWidgets('ExpandIcon has correct semantic hints', (WidgetTester tester) async { + testWidgets('Material2 - ExpandIcon has correct semantic hints', (WidgetTester tester) async { final SemanticsHandle handle = tester.ensureSemantics(); const DefaultMaterialLocalizations localizations = DefaultMaterialLocalizations(); await tester.pumpWidget(wrap( @@ -194,6 +225,7 @@ void main() { )); await tester.pumpWidget(wrap( + theme: ThemeData(useMaterial3: false), child: ExpandIcon( onPressed: (bool _) { }, ), @@ -210,6 +242,52 @@ void main() { handle.dispose(); }); + testWidgets('Material3 - ExpandIcon has correct semantic hints', (WidgetTester tester) async { + final SemanticsHandle handle = tester.ensureSemantics(); + const DefaultMaterialLocalizations localizations = DefaultMaterialLocalizations(); + + await tester.pumpWidget(wrap( + child: ExpandIcon( + isExpanded: true, + onPressed: (bool _) { }, + ), + )); + + expect(tester.getSemantics(find.byType(ExpandIcon)), matchesSemantics( + onTapHint: localizations.expandedIconTapHint, + children: [ + matchesSemantics( + hasTapAction: true, + hasEnabledState: true, + isEnabled: true, + isFocusable: true, + isButton: true, + ), + ], + )); + + await tester.pumpWidget(wrap( + child: ExpandIcon( + onPressed: (bool _) { }, + ), + )); + + expect(tester.getSemantics(find.byType(ExpandIcon)), matchesSemantics( + onTapHint: localizations.collapsedIconTapHint, + children: [ + matchesSemantics( + hasTapAction: true, + hasEnabledState: true, + isEnabled: true, + isFocusable: true, + isButton: true, + ), + ], + )); + + handle.dispose(); + }); + testWidgets('ExpandIcon uses custom icon color and expanded icon color', (WidgetTester tester) async { bool expanded = false; IconTheme iconTheme; diff --git a/packages/flutter/test/material/expansion_panel_test.dart b/packages/flutter/test/material/expansion_panel_test.dart index 7c2fc32a49bd..d2543b030466 100644 --- a/packages/flutter/test/material/expansion_panel_test.dart +++ b/packages/flutter/test/material/expansion_panel_test.dart @@ -178,7 +178,7 @@ void main() { expect(box.size.height - oldHeight, greaterThanOrEqualTo(100.0)); // 100 + some margin }); - testWidgets('ExpansionPanelList does not merge header when canTapOnHeader is false', (WidgetTester tester) async { + testWidgets('Material2 - ExpansionPanelList does not merge header when canTapOnHeader is false', (WidgetTester tester) async { final SemanticsHandle handle = tester.ensureSemantics(); final Key headerKey = UniqueKey(); await tester.pumpWidget( @@ -226,6 +226,58 @@ void main() { handle.dispose(); }); + testWidgets('Material3 - ExpansionPanelList does not merge header when canTapOnHeader is false', (WidgetTester tester) async { + final SemanticsHandle handle = tester.ensureSemantics(); + final Key headerKey = UniqueKey(); + await tester.pumpWidget( + MaterialApp( + home: ExpansionPanelListSemanticsTest(headerKey: headerKey), + ), + ); + + // Make sure custom gesture detector widget is clickable. + await tester.tap(find.text('head1')); + await tester.pump(); + + final ExpansionPanelListSemanticsTestState state = + tester.state(find.byType(ExpansionPanelListSemanticsTest)); + expect(state.headerTapped, true); + + // Check the expansion icon semantics does not merged with header widget. + final Finder expansionIcon = find.descendant( + of: find.ancestor( + of: find.byKey(headerKey), + matching: find.byType(Row), + ), + matching: find.byType(ExpandIcon), + ); + + expect(tester.getSemantics(expansionIcon), matchesSemantics( + label: 'Expand', + children: [ + matchesSemantics( + isButton: true, + hasEnabledState: true, + isEnabled: true, + isFocusable: true, + hasTapAction: true, + ), + ], + )); + + // Check custom header widget semantics is preserved. + final Finder headerWidget = find.descendant( + of: find.byKey(headerKey), + matching: find.byType(RichText), + ); + expect(tester.getSemantics(headerWidget), matchesSemantics( + label: 'head1', + hasTapAction: true, + )); + + handle.dispose(); + }); + testWidgets('Multiple Panel List test', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( @@ -998,7 +1050,7 @@ void main() { await tester.pumpAndSettle(); }); - testWidgets('Panel header has semantics, canTapOnHeader = false ', (WidgetTester tester) async { + testWidgets('Material2 - Panel header has semantics, canTapOnHeader = false', (WidgetTester tester) async { const Key expandedKey = Key('expanded'); const Key collapsedKey = Key('collapsed'); const DefaultMaterialLocalizations localizations = DefaultMaterialLocalizations(); @@ -1083,6 +1135,99 @@ void main() { handle.dispose(); }); + testWidgets('Material3 - Panel header has semantics, canTapOnHeader = false', (WidgetTester tester) async { + const Key expandedKey = Key('expanded'); + const Key collapsedKey = Key('collapsed'); + const DefaultMaterialLocalizations localizations = DefaultMaterialLocalizations(); + final SemanticsHandle handle = tester.ensureSemantics(); + final List demoItems = [ + ExpansionPanel( + headerBuilder: (BuildContext context, bool isExpanded) { + return const Text('Expanded', key: expandedKey); + }, + body: const SizedBox(height: 100.0), + isExpanded: true, + ), + ExpansionPanel( + headerBuilder: (BuildContext context, bool isExpanded) { + return const Text('Collapsed', key: collapsedKey); + }, + body: const SizedBox(height: 100.0), + ), + ]; + + final ExpansionPanelList expansionList = ExpansionPanelList( + children: demoItems, + ); + + await tester.pumpWidget( + MaterialApp( + home: SingleChildScrollView( + child: expansionList, + ), + ), + ); + + // Check the semantics of [ExpandIcon] for expanded panel. + final Finder expandedIcon = find.descendant( + of: find.ancestor( + of: find.byKey(expandedKey), + matching: find.byType(Row), + ), + matching: find.byType(ExpandIcon), + ); + + expect(tester.getSemantics(expandedIcon), matchesSemantics( + label: 'Collapse', + onTapHint: localizations.expandedIconTapHint, + children: [ + matchesSemantics( + isButton: true, + hasEnabledState: true, + isEnabled: true, + isFocusable: true, + hasTapAction: true, + ), + ], + )); + + // Check the semantics of the header widget for expanded panel. + final Finder expandedHeader = find.byKey(expandedKey); + expect(tester.getSemantics(expandedHeader), matchesSemantics( + label: 'Expanded', + )); + + // Check the semantics of [ExpandIcon] for collapsed panel. + final Finder collapsedIcon = find.descendant( + of: find.ancestor( + of: find.byKey(collapsedKey), + matching: find.byType(Row), + ), + matching: find.byType(ExpandIcon), + ); + expect(tester.getSemantics(collapsedIcon), matchesSemantics( + label: 'Expand', + onTapHint: localizations.collapsedIconTapHint, + children: [ + matchesSemantics( + isButton: true, + hasEnabledState: true, + isEnabled: true, + isFocusable: true, + hasTapAction: true, + ), + ], + )); + + // Check the semantics of the header widget for expanded panel. + final Finder collapsedHeader = find.byKey(collapsedKey); + expect(tester.getSemantics(collapsedHeader), matchesSemantics( + label: 'Collapsed', + )); + + handle.dispose(); + }); + testWidgets('Panel header has semantics, canTapOnHeader = true', (WidgetTester tester) async { const Key expandedKey = Key('expanded'); const Key collapsedKey = Key('collapsed'); diff --git a/packages/flutter/test/material/toggle_buttons_test.dart b/packages/flutter/test/material/toggle_buttons_test.dart index 5bc6b533e5de..0e38e4612e58 100644 --- a/packages/flutter/test/material/toggle_buttons_test.dart +++ b/packages/flutter/test/material/toggle_buttons_test.dart @@ -7,6 +7,7 @@ @Tags(['reduced-test-set']) library; +import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; @@ -16,13 +17,12 @@ import '../widgets/semantics_tester.dart'; const double _defaultBorderWidth = 1.0; Widget boilerplate({ - bool? useMaterial3, + ThemeData? theme, MaterialTapTargetSize? tapTargetSize, required Widget child, }) { return Theme( - data: ThemeData( - useMaterial3: useMaterial3, + data: theme ?? ThemeData( materialTapTargetSize: tapTargetSize, ), child: Directionality( @@ -1273,12 +1273,12 @@ void main() { } }); - testWidgets('ToggleButtons text baseline alignment', (WidgetTester tester) async { + testWidgets('Material2 - ToggleButtons text baseline alignment', (WidgetTester tester) async { // The point size of the fonts must be a multiple of 4 until // https://github.com/flutter/flutter/issues/122066 is resolved. await tester.pumpWidget( boilerplate( - useMaterial3: false, + theme: ThemeData(useMaterial3: false), child: Row( crossAxisAlignment: CrossAxisAlignment.baseline, textBaseline: TextBaseline.alphabetic, @@ -1323,6 +1323,55 @@ void main() { expect(firstToggleButtonDy, textDy - 5.0); }); + testWidgets('Material3 - ToggleButtons text baseline alignment', (WidgetTester tester) async { + // The point size of the fonts must be a multiple of 4 until + // https://github.com/flutter/flutter/issues/122066 is resolved. + await tester.pumpWidget( + boilerplate( + child: Row( + crossAxisAlignment: CrossAxisAlignment.baseline, + textBaseline: TextBaseline.alphabetic, + children: [ + ToggleButtons( + borderWidth: 5.0, + isSelected: const [false, true], + children: const [ + Text('First child', style: TextStyle(fontFamily: 'FlutterTest', fontSize: 8.0)), + Text('Second child', style: TextStyle(fontFamily: 'FlutterTest', fontSize: 8.0)), + ], + ), + ElevatedButton( + onPressed: null, + style: ElevatedButton.styleFrom(textStyle: const TextStyle( + fontFamily: 'FlutterTest', + fontSize: 20.0, + )), + child: const Text('Elevated Button'), + ), + const Text('Text', style: TextStyle(fontFamily: 'FlutterTest', fontSize: 28.0)), + ], + ), + ), + ); + + // The test font extends 0.25 * fontSize below the baseline. + // So the three row elements line up like this: + // + // ToggleButton MaterialButton Text + // ------------------------------------ baseline + // 2.0 5.0 7.0 space below the baseline = 0.25 * fontSize + // ------------------------------------ widget text dy values + + final double firstToggleButtonDy = tester.getBottomLeft(find.text('First child')).dy; + final double secondToggleButtonDy = tester.getBottomLeft(find.text('Second child')).dy; + final double elevatedButtonDy = tester.getBottomLeft(find.text('Elevated Button')).dy; + final double textDy = tester.getBottomLeft(find.text('Text')).dy; + + expect(firstToggleButtonDy, secondToggleButtonDy); + expect(firstToggleButtonDy, closeTo(elevatedButtonDy - 1.7, 0.1)); + expect(firstToggleButtonDy, closeTo(textDy - 9.7, 0.1)); + }, skip: kIsWeb && !isCanvasKit); // https://github.com/flutter/flutter/issues/99933 + testWidgets('Directionality test', (WidgetTester tester) async { await tester.pumpWidget( Material( @@ -1564,10 +1613,10 @@ void main() { }, ); - testWidgets('Tap target size is configurable by ThemeData.materialTapTargetSize', (WidgetTester tester) async { + testWidgets('Material2 - Tap target size is configurable by ThemeData.materialTapTargetSize', (WidgetTester tester) async { Widget buildFrame(MaterialTapTargetSize tapTargetSize, Key key) { return boilerplate( - useMaterial3: false, + theme: ThemeData(useMaterial3: false), tapTargetSize: tapTargetSize, child: ToggleButtons( key: key, @@ -1589,13 +1638,40 @@ void main() { final Key key2 = UniqueKey(); await tester.pumpWidget(buildFrame(MaterialTapTargetSize.shrinkWrap, key2)); - expect(tester.getSize(find.byKey(key2)), const Size(228.0, 34.0)); + expect(tester.getSize(find.byKey(key2)), const Size(228.0, 48.0)); + }); + + testWidgets('Material3 - Tap target size is configurable by ThemeData.materialTapTargetSize', (WidgetTester tester) async { + Widget buildFrame(MaterialTapTargetSize tapTargetSize, Key key) { + return boilerplate( + tapTargetSize: tapTargetSize, + child: ToggleButtons( + key: key, + constraints: const BoxConstraints(minWidth: 32.0, minHeight: 32.0), + isSelected: const [false, true, false], + onPressed: (int index) {}, + children: const [ + Text('First'), + Text('Second'), + Text('Third'), + ], + ), + ); + } + + final Key key1 = UniqueKey(); + await tester.pumpWidget(buildFrame(MaterialTapTargetSize.padded, key1)); + expect(tester.getSize(find.byKey(key1)), const Size(232.0, 48.0)); + + final Key key2 = UniqueKey(); + await tester.pumpWidget(buildFrame(MaterialTapTargetSize.shrinkWrap, key2)); + expect(tester.getSize(find.byKey(key2)), const Size(232.0, 34.0)); }); - testWidgets('Tap target size is configurable', (WidgetTester tester) async { + testWidgets('Material2 - Tap target size is configurable', (WidgetTester tester) async { Widget buildFrame(MaterialTapTargetSize tapTargetSize, Key key) { return boilerplate( - useMaterial3: false, + theme: ThemeData(useMaterial3: false), child: ToggleButtons( key: key, tapTargetSize: tapTargetSize, @@ -1620,6 +1696,33 @@ void main() { expect(tester.getSize(find.byKey(key2)), const Size(228.0, 34.0)); }); + testWidgets('Material3 - Tap target size is configurable', (WidgetTester tester) async { + Widget buildFrame(MaterialTapTargetSize tapTargetSize, Key key) { + return boilerplate( + child: ToggleButtons( + key: key, + tapTargetSize: tapTargetSize, + constraints: const BoxConstraints(minWidth: 32.0, minHeight: 32.0), + isSelected: const [false, true, false], + onPressed: (int index) {}, + children: const [ + Text('First'), + Text('Second'), + Text('Third'), + ], + ), + ); + } + + final Key key1 = UniqueKey(); + await tester.pumpWidget(buildFrame(MaterialTapTargetSize.padded, key1)); + expect(tester.getSize(find.byKey(key1)), const Size(232.0, 48.0)); + + final Key key2 = UniqueKey(); + await tester.pumpWidget(buildFrame(MaterialTapTargetSize.shrinkWrap, key2)); + expect(tester.getSize(find.byKey(key2)), const Size(232.0, 34.0)); + }); + testWidgets('Tap target size is configurable for vertical axis', (WidgetTester tester) async { Widget buildFrame(MaterialTapTargetSize tapTargetSize, Key key) { return boilerplate( @@ -1649,11 +1752,55 @@ void main() { }); // Regression test for https://github.com/flutter/flutter/issues/73725 - testWidgets('Border radius paint test when there is only one button', (WidgetTester tester) async { + testWidgets('Material2 - Border radius paint test when there is only one button', (WidgetTester tester) async { final ThemeData theme = ThemeData(useMaterial3: false); await tester.pumpWidget( boilerplate( - useMaterial3: false, + theme: theme, + child: RepaintBoundary( + child: ToggleButtons( + borderRadius: const BorderRadius.all(Radius.circular(7.0)), + isSelected: const [true], + onPressed: (int index) {}, + children: const [ + Text('First child'), + ], + ), + ), + ), + ); + + // The only button should be laid out at the center of the screen. + expect(tester.getCenter(find.text('First child')), const Offset(400.0, 300.0)); + + final List toggleButtonRenderObject = tester.allRenderObjects.where((RenderObject object) { + return object.runtimeType.toString() == '_SelectToggleButtonRenderObject'; + }).toSet().toList(); + + // The first button paints the left, top and right sides with a path. + expect( + toggleButtonRenderObject[0], + paints + // physical model paints + ..path() + // left side, top and right - enabled. + ..path( + style: PaintingStyle.stroke, + color: theme.colorScheme.onSurface.withOpacity(0.12), + strokeWidth: _defaultBorderWidth, + ), + ); + + await expectLater( + find.byType(RepaintBoundary), + matchesGoldenFile('m2_toggle_buttons.oneButton.boardsPaint.png'), + ); + }); + + testWidgets('Material3 - Border radius paint test when there is only one button', (WidgetTester tester) async { + final ThemeData theme = ThemeData(); + await tester.pumpWidget( + boilerplate( child: RepaintBoundary( child: ToggleButtons( borderRadius: const BorderRadius.all(Radius.circular(7.0)), @@ -1690,14 +1837,41 @@ void main() { await expectLater( find.byType(RepaintBoundary), - matchesGoldenFile('toggle_buttons.oneButton.boardsPaint.png'), + matchesGoldenFile('m3_toggle_buttons.oneButton.boardsPaint.png'), + ); + }); + + testWidgets('Material2 - Border radius paint test when Radius.x or Radius.y equal 0.0', (WidgetTester tester) async { + await tester.pumpWidget( + boilerplate( + theme: ThemeData(useMaterial3: false), + child: RepaintBoundary( + child: ToggleButtons( + borderRadius: const BorderRadius.only( + topRight: Radius.elliptical(10, 0), + topLeft: Radius.elliptical(0, 10), + bottomRight: Radius.elliptical(0, 10), + bottomLeft: Radius.elliptical(10, 0), + ), + isSelected: const [true], + onPressed: (int index) {}, + children: const [ + Text('First child'), + ], + ), + ), + ), + ); + + await expectLater( + find.byType(RepaintBoundary), + matchesGoldenFile('m2_toggle_buttons.oneButton.boardsPaint2.png'), ); }); - testWidgets('Border radius paint test when Radius.x or Radius.y equal 0.0', (WidgetTester tester) async { + testWidgets('Material3 - Border radius paint test when Radius.x or Radius.y equal 0.0', (WidgetTester tester) async { await tester.pumpWidget( boilerplate( - useMaterial3: false, child: RepaintBoundary( child: ToggleButtons( borderRadius: const BorderRadius.only( @@ -1718,7 +1892,7 @@ void main() { await expectLater( find.byType(RepaintBoundary), - matchesGoldenFile('toggle_buttons.oneButton.boardsPaint2.png'), + matchesGoldenFile('m3_toggle_buttons.oneButton.boardsPaint2.png'), ); });