From 9c3bfde5c0703fa7a392d09474d8c8d4d9c604e0 Mon Sep 17 00:00:00 2001 From: Hans Muller Date: Fri, 9 Jun 2023 10:11:23 -0700 Subject: [PATCH] Updated flutter_localizations tests for Material3; (#128521) Updated the localization tests so that they'll DTRT when useMaterial3:true becomes the default for ThemeData. In a few cases there are M2 and M3 tests now, to check features that are significantly different in Material3, notably the double ring for the 24 hour input dial. | Material 2 | Material 3| |---------|---------| | Screenshot 2023-06-08 at 10 47 37 AM | Screenshot 2023-06-08 at 10 47 13 AM | In M3, most aspects of the ideographic text styles are the same as for alphabetic styles, so there are some tweaks here to account for that. --- .../test/basics_test.dart | 12 +- .../test/material/date_picker_test.dart | 231 ++++++++++++------ .../test/material/time_picker_test.dart | 116 ++++++--- .../flutter_localizations/test/text_test.dart | 34 +-- 4 files changed, 258 insertions(+), 135 deletions(-) diff --git a/packages/flutter_localizations/test/basics_test.dart b/packages/flutter_localizations/test/basics_test.dart index 4ac2375a7d..3ab92f6a4e 100644 --- a/packages/flutter_localizations/test/basics_test.dart +++ b/packages/flutter_localizations/test/basics_test.dart @@ -9,6 +9,7 @@ import 'package:flutter_test/flutter_test.dart'; void main() { testWidgets('Nested Localizations', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( // Creates the outer Localizations widget. + theme: ThemeData(useMaterial3: true), home: ListView( children: [ const LocalizationTracker(key: ValueKey('outer')), @@ -20,11 +21,12 @@ void main() { ], ), )); - + // Most localized aspects of the TextTheme text styles are the same for the default US local and + // for Chinese for Material3. The baselines for all text styles differ. final LocalizationTrackerState outerTracker = tester.state(find.byKey(const ValueKey('outer'), skipOffstage: false)); - expect(outerTracker.bodySmallFontSize, 12.0); + expect(outerTracker.textBaseline, TextBaseline.alphabetic); final LocalizationTrackerState innerTracker = tester.state(find.byKey(const ValueKey('inner'), skipOffstage: false)); - expect(innerTracker.bodySmallFontSize, 13.0); + expect(innerTracker.textBaseline, TextBaseline.ideographic); }); testWidgets('Localizations is compatible with ChangeNotifier.dispose() called during didChangeDependencies', (WidgetTester tester) async { @@ -92,11 +94,11 @@ class LocalizationTracker extends StatefulWidget { } class LocalizationTrackerState extends State { - late double bodySmallFontSize; + late TextBaseline textBaseline; @override Widget build(BuildContext context) { - bodySmallFontSize = Theme.of(context).textTheme.bodySmall!.fontSize!; + textBaseline = Theme.of(context).textTheme.bodySmall!.textBaseline!; return Container(); } } diff --git a/packages/flutter_localizations/test/material/date_picker_test.dart b/packages/flutter_localizations/test/material/date_picker_test.dart index 4d7984465c..bfae7a2dba 100644 --- a/packages/flutter_localizations/test/material/date_picker_test.dart +++ b/packages/flutter_localizations/test/material/date_picker_test.dart @@ -93,79 +93,123 @@ void main() { }); testWidgets('locale parameter overrides ambient locale', (WidgetTester tester) async { - await tester.pumpWidget(MaterialApp( - locale: const Locale('en', 'US'), - supportedLocales: const [ - Locale('en', 'US'), - Locale('fr', 'CA'), - ], - localizationsDelegates: GlobalMaterialLocalizations.delegates, - home: Material( - child: Builder( - builder: (BuildContext context) { - return TextButton( - onPressed: () async { - await showDatePicker( - context: context, - initialDate: initialDate, - firstDate: firstDate, - lastDate: lastDate, - locale: const Locale('fr', 'CA'), - ); - }, - child: const Text('X'), - ); - }, + Widget buildFrame(bool useMaterial3) { + return MaterialApp( + theme: ThemeData(useMaterial3: useMaterial3), + locale: const Locale('en', 'US'), + supportedLocales: const [ + Locale('en', 'US'), + Locale('fr', 'CA'), + ], + localizationsDelegates: GlobalMaterialLocalizations.delegates, + home: Material( + child: Builder( + builder: (BuildContext context) { + return TextButton( + onPressed: () async { + await showDatePicker( + context: context, + initialDate: initialDate, + firstDate: firstDate, + lastDate: lastDate, + locale: const Locale('fr', 'CA'), + ); + }, + child: const Text('X'), + ); + }, + ), ), - ), - )); + ); + } + Element getPicker() => tester.element(find.byType(CalendarDatePicker)); + + await tester.pumpWidget(buildFrame(true)); await tester.tap(find.text('X')); - await tester.pumpAndSettle(const Duration(seconds: 1)); + await tester.pumpAndSettle(); - final Element picker = tester.element(find.byType(CalendarDatePicker)); expect( - Localizations.localeOf(picker), + Localizations.localeOf(getPicker()), const Locale('fr', 'CA'), ); + expect( + Directionality.of(getPicker()), + TextDirection.ltr, + ); + + await tester.tap(find.text('Annuler')); + + // The tests below are only relevant for Material 2. Once Material 2 + // support is deprecated and the APIs are removed, these tests + // can be deleted. + + await tester.pumpWidget(buildFrame(false)); + await tester.tap(find.text('X')); + await tester.pumpAndSettle(); expect( - Directionality.of(picker), + Localizations.localeOf(getPicker()), + const Locale('fr', 'CA'), + ); + expect( + Directionality.of(getPicker()), TextDirection.ltr, ); await tester.tap(find.text('ANNULER')); + }); testWidgets('textDirection parameter overrides ambient textDirection', (WidgetTester tester) async { - await tester.pumpWidget(MaterialApp( - locale: const Locale('en', 'US'), - home: Material( - child: Builder( - builder: (BuildContext context) { - return TextButton( - onPressed: () async { - await showDatePicker( - context: context, - initialDate: initialDate, - firstDate: firstDate, - lastDate: lastDate, - textDirection: TextDirection.rtl, - ); - }, - child: const Text('X'), - ); - }, + Widget buildFrame(bool useMaterial3) { + return MaterialApp( + theme: ThemeData(useMaterial3: useMaterial3), + locale: const Locale('en', 'US'), + home: Material( + child: Builder( + builder: (BuildContext context) { + return TextButton( + onPressed: () async { + await showDatePicker( + context: context, + initialDate: initialDate, + firstDate: firstDate, + lastDate: lastDate, + textDirection: TextDirection.rtl, + ); + }, + child: const Text('X'), + ); + }, + ), ), - ), - )); + ); + } + Element getPicker() => tester.element(find.byType(CalendarDatePicker)); + + await tester.pumpWidget(buildFrame(true)); await tester.tap(find.text('X')); - await tester.pumpAndSettle(const Duration(seconds: 1)); + await tester.pumpAndSettle(); - final Element picker = tester.element(find.byType(CalendarDatePicker)); expect( - Directionality.of(picker), + Directionality.of(getPicker()), + TextDirection.rtl, + ); + + await tester.tap(find.text('Cancel')); + + // The tests below are only relevant for Material 2. Once Material 2 + // support is deprecated and the APIs are removed, these tests + // can be deleted. + + await tester.pumpWidget(buildFrame(false)); + await tester.tap(find.text('X')); + await tester.pumpAndSettle(); + + expect( + Directionality.of(getPicker()), TextDirection.rtl, ); @@ -173,45 +217,70 @@ void main() { }); testWidgets('textDirection parameter takes precedence over locale parameter', (WidgetTester tester) async { - await tester.pumpWidget(MaterialApp( - locale: const Locale('en', 'US'), - supportedLocales: const [ - Locale('en', 'US'), - Locale('fr', 'CA'), - ], - localizationsDelegates: GlobalMaterialLocalizations.delegates, - home: Material( - child: Builder( - builder: (BuildContext context) { - return TextButton( - onPressed: () async { - await showDatePicker( - context: context, - initialDate: initialDate, - firstDate: firstDate, - lastDate: lastDate, - locale: const Locale('fr', 'CA'), - textDirection: TextDirection.rtl, - ); - }, - child: const Text('X'), - ); - }, + Widget buildFrame(bool useMaterial3) { + return MaterialApp( + theme: ThemeData(useMaterial3: useMaterial3), + locale: const Locale('en', 'US'), + supportedLocales: const [ + Locale('en', 'US'), + Locale('fr', 'CA'), + ], + localizationsDelegates: GlobalMaterialLocalizations.delegates, + home: Material( + child: Builder( + builder: (BuildContext context) { + return TextButton( + onPressed: () async { + await showDatePicker( + context: context, + initialDate: initialDate, + firstDate: firstDate, + lastDate: lastDate, + locale: const Locale('fr', 'CA'), + textDirection: TextDirection.rtl, + ); + }, + child: const Text('X'), + ); + }, + ), ), - ), - )); + ); + } + Element getPicker() => tester.element(find.byType(CalendarDatePicker)); + + await tester.pumpWidget(buildFrame(true)); await tester.tap(find.text('X')); await tester.pumpAndSettle(const Duration(seconds: 1)); - final Element picker = tester.element(find.byType(CalendarDatePicker)); expect( - Localizations.localeOf(picker), + Localizations.localeOf(getPicker()), const Locale('fr', 'CA'), ); expect( - Directionality.of(picker), + Directionality.of(getPicker()), + TextDirection.rtl, + ); + + await tester.tap(find.text('Annuler')); + + // The tests below are only relevant for Material 2. Once Material 2 + // support is deprecated and the APIs are removed, these tests + // can be deleted. + + await tester.pumpWidget(buildFrame(false)); + await tester.tap(find.text('X')); + await tester.pumpAndSettle(); + + expect( + Localizations.localeOf(getPicker()), + const Locale('fr', 'CA'), + ); + + expect( + Directionality.of(getPicker()), TextDirection.rtl, ); diff --git a/packages/flutter_localizations/test/material/time_picker_test.dart b/packages/flutter_localizations/test/material/time_picker_test.dart index 3a07db3fbd..b23019d71a 100644 --- a/packages/flutter_localizations/test/material/time_picker_test.dart +++ b/packages/flutter_localizations/test/material/time_picker_test.dart @@ -220,7 +220,7 @@ void main() { } }); - testWidgets('uses single-ring 24-hour dial for all formats', (WidgetTester tester) async { + testWidgets('Material2 uses single-ring 24-hour dial for all locales', (WidgetTester tester) async { const List locales = [ Locale('en', 'US'), // h Locale('en', 'GB'), // HH @@ -228,11 +228,11 @@ void main() { ]; for (final Locale locale in locales) { // Tap along the segment stretching from the center to the edge at - // 12:00 AM position. Because there's only one ring, no matter where you - // tap the time will be the same. + // 12:00 AM position. Because there's only one ring, in the M2 + // DatePicker no matter where you tap the time will be the same. for (int i = 1; i < 10; i++) { TimeOfDay? result; - final Offset center = await startPicker(tester, (TimeOfDay? time) { result = time; }, locale: locale); + final Offset center = await startPicker(tester, (TimeOfDay? time) { result = time; }, locale: locale, useMaterial3: false); final Size size = tester.getSize(find.byKey(const Key('time-picker-dial'))); final double dy = (size.height / 2.0 / 10) * i; await tester.tapAt(Offset(center.dx, center.dy - dy)); @@ -242,34 +242,57 @@ void main() { } }); - const List labels12To11 = ['12', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11']; - const List labels00To22TwoDigit = ['00', '02', '04', '06', '08', '10', '12', '14', '16', '18', '20', '22']; + testWidgets('Material3 uses a double-ring 24-hour dial for 24 hour locales', (WidgetTester tester) async { + Future testLocale(Locale locale, int startFactor, int endFactor, TimeOfDay expectedTime) async { + // For locales that display 24 hour time, factors 1-5 put the tap on the + // inner ring's "12" (the inner ring goes from 12-23). Otherwise the offset + // should land on the outer ring's "00". + for (int factor = startFactor; factor < endFactor; factor += 1) { + TimeOfDay? result; + final Offset center = await startPicker(tester, (TimeOfDay? time) { result = time; }, locale: locale, useMaterial3: true); + final Size size = tester.getSize(find.byKey(const Key('time-picker-dial'))); + final double dy = (size.height / 2.0 / 10) * factor; + await tester.tapAt(Offset(center.dx, center.dy - dy)); + await finishPicker(tester); + expect(result, equals(expectedTime), reason: 'Failed for locale=$locale with factor=$factor'); + } + } - Future mediaQueryBoilerplate(WidgetTester tester, bool alwaysUse24HourFormat) async { + await testLocale(const Locale('en', 'US'), 1, 10, const TimeOfDay(hour: 0, minute: 0)); // 12 hour + await testLocale(const Locale('en', 'ES'), 1, 10, const TimeOfDay(hour: 0, minute: 0)); // 12 hour + await testLocale(const Locale('en', 'GB'), 1, 5, const TimeOfDay(hour: 12, minute: 0)); // 24 hour, inner ring + await testLocale(const Locale('en', 'GB'), 6, 10, const TimeOfDay(hour: 0, minute: 0)); // 24 hour, outer ring + }); + + const List labels12To11 = ['12', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11']; + const List labels00To22TwoDigit = ['00', '02', '04', '06', '08', '10', '12', '14', '16', '18', '20', '22']; // Material 2 + const List labels00To23TwoDigit = [ // Material 3 + '00', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15', '16', '17', '18', '19', '20', '21', '22', '23']; + + Future mediaQueryBoilerplate(WidgetTester tester, {required bool alwaysUse24HourFormat, bool? useMaterial3}) async { await tester.pumpWidget( - Localizations( - locale: const Locale('en', 'US'), - delegates: const >[ - GlobalMaterialLocalizations.delegate, - DefaultWidgetsLocalizations.delegate, - ], - child: MediaQuery( - data: MediaQueryData(alwaysUse24HourFormat: alwaysUse24HourFormat), - child: Material( - child: Directionality( - textDirection: TextDirection.ltr, - child: Navigator( - onGenerateRoute: (RouteSettings settings) { - return MaterialPageRoute(builder: (BuildContext context) { - return TextButton( - onPressed: () { - showTimePicker(context: context, initialTime: const TimeOfDay(hour: 7, minute: 0)); - }, - child: const Text('X'), - ); - }); - }, - ), + MaterialApp( + theme: ThemeData(useMaterial3: useMaterial3), + builder: (BuildContext context, Widget? child) { + return MediaQuery( + data: MediaQueryData(alwaysUse24HourFormat: alwaysUse24HourFormat), + child: child!, + ); + }, + home: Material( + child: Directionality( + textDirection: TextDirection.ltr, + child: Navigator( + onGenerateRoute: (RouteSettings settings) { + return MaterialPageRoute(builder: (BuildContext context) { + return TextButton( + onPressed: () { + showTimePicker(context: context, initialTime: const TimeOfDay(hour: 7, minute: 0)); + }, + child: const Text('X'), + ); + }); + }, ), ), ), @@ -281,7 +304,7 @@ void main() { } testWidgets('respects MediaQueryData.alwaysUse24HourFormat == false', (WidgetTester tester) async { - await mediaQueryBoilerplate(tester, false); + await mediaQueryBoilerplate(tester, alwaysUse24HourFormat: false); final CustomPaint dialPaint = tester.widget(find.byKey(const ValueKey('time-picker-dial'))); final dynamic dialPainter = dialPaint.painter; @@ -302,8 +325,30 @@ void main() { ); }); - testWidgets('respects MediaQueryData.alwaysUse24HourFormat == true', (WidgetTester tester) async { - await mediaQueryBoilerplate(tester, true); + testWidgets('Material3 respects MediaQueryData.alwaysUse24HourFormat == true', (WidgetTester tester) async { + await mediaQueryBoilerplate(tester, alwaysUse24HourFormat: true, useMaterial3: true); + + final CustomPaint dialPaint = tester.widget(find.byKey(const ValueKey('time-picker-dial'))); + final dynamic dialPainter = dialPaint.painter; + // ignore: avoid_dynamic_calls + final List primaryLabels = dialPainter.primaryLabels as List; + expect( + // ignore: avoid_dynamic_calls + primaryLabels.map((dynamic tp) => ((tp.painter as TextPainter).text! as TextSpan).text!), + labels00To23TwoDigit, + ); + + // ignore: avoid_dynamic_calls + final List selectedLabels = dialPainter.selectedLabels as List; + expect( + // ignore: avoid_dynamic_calls + selectedLabels.map((dynamic tp) => ((tp.painter as TextPainter).text! as TextSpan).text!), + labels00To23TwoDigit, + ); + }); + + testWidgets('Material2 respects MediaQueryData.alwaysUse24HourFormat == true', (WidgetTester tester) async { + await mediaQueryBoilerplate(tester, alwaysUse24HourFormat: true, useMaterial3: false); final CustomPaint dialPaint = tester.widget(find.byKey(const ValueKey('time-picker-dial'))); final dynamic dialPainter = dialPaint.painter; @@ -330,15 +375,18 @@ class _TimePickerLauncher extends StatelessWidget { this.onChanged, required this.locale, this.entryMode = TimePickerEntryMode.dial, + this.useMaterial3, }); final ValueChanged? onChanged; final Locale locale; final TimePickerEntryMode entryMode; + final bool? useMaterial3; @override Widget build(BuildContext context) { return MaterialApp( + theme: ThemeData(useMaterial3: useMaterial3), locale: locale, supportedLocales: [locale], localizationsDelegates: GlobalMaterialLocalizations.delegates, @@ -368,11 +416,13 @@ Future startPicker( WidgetTester tester, ValueChanged onChanged, { Locale locale = const Locale('en', 'US'), + bool? useMaterial3, }) async { await tester.pumpWidget( _TimePickerLauncher( onChanged: onChanged, locale: locale, + useMaterial3: useMaterial3, ), ); await tester.tap(find.text('X')); diff --git a/packages/flutter_localizations/test/text_test.dart b/packages/flutter_localizations/test/text_test.dart index b0501f048b..310cdf0ea2 100644 --- a/packages/flutter_localizations/test/text_test.dart +++ b/packages/flutter_localizations/test/text_test.dart @@ -17,6 +17,7 @@ void main() { final Key targetKey = UniqueKey(); await tester.pumpWidget( MaterialApp( + theme: ThemeData(useMaterial3: true), routes: { '/next': (BuildContext context) { return const Text('Next'); @@ -75,20 +76,20 @@ void main() { Offset bottomLeft = tester.getBottomLeft(find.text('hello, world')); Offset bottomRight = tester.getBottomRight(find.text('hello, world')); - expect(topLeft, const Offset(392.0, 299.5)); - expect(topRight, const Offset(596.0, 299.5)); - expect(bottomLeft, const Offset(392.0, 316.5)); - expect(bottomRight, const Offset(596.0, 316.5)); + expect(topLeft, const Offset(392.0, 298.0)); + expect(topRight, const Offset(562.0, 298.0)); + expect(bottomLeft, const Offset(392.0, 318.0)); + expect(bottomRight, const Offset(562.0, 318.0)); topLeft = tester.getTopLeft(find.text('你好,世界')); topRight = tester.getTopRight(find.text('你好,世界')); bottomLeft = tester.getBottomLeft(find.text('你好,世界')); bottomRight = tester.getBottomRight(find.text('你好,世界')); - expect(topLeft, const Offset(392.0, 347.5)); - expect(topRight, const Offset(477.0, 347.5)); - expect(bottomLeft, const Offset(392.0, 364.5)); - expect(bottomRight, const Offset(477.0, 364.5)); + expect(topLeft, const Offset(392.0, 346.0)); + expect(topRight, const Offset(463.0, 346.0)); + expect(bottomLeft, const Offset(392.0, 366.0)); + expect(bottomRight, const Offset(463.0, 366.0)); }); testWidgets('Text baseline with EN locale', (WidgetTester tester) async { @@ -101,6 +102,7 @@ void main() { final Key targetKey = UniqueKey(); await tester.pumpWidget( MaterialApp( + theme: ThemeData(useMaterial3: true), routes: { '/next': (BuildContext context) { return const Text('Next'); @@ -159,19 +161,19 @@ void main() { Offset bottomLeft = tester.getBottomLeft(find.text('hello, world')); Offset bottomRight = tester.getBottomRight(find.text('hello, world')); - expect(topLeft, const Offset(392.0, 300.0)); - expect(topRight, const Offset(584.0, 300.0)); - expect(bottomLeft, const Offset(392.0, 316)); - expect(bottomRight, const Offset(584.0, 316)); + expect(topLeft, const Offset(392.0, 298.0)); + expect(topRight, const Offset(562.0, 298.0)); + expect(bottomLeft, const Offset(392.0, 318.0)); + expect(bottomRight, const Offset(562.0, 318.0)); topLeft = tester.getTopLeft(find.text('你好,世界')); topRight = tester.getTopRight(find.text('你好,世界')); bottomLeft = tester.getBottomLeft(find.text('你好,世界')); bottomRight = tester.getBottomRight(find.text('你好,世界')); - expect(topLeft, const Offset(392.0, 348.0)); - expect(topRight, const Offset(472.0, 348.0)); - expect(bottomLeft, const Offset(392.0, 364.0)); - expect(bottomRight, const Offset(472.0, 364.0)); + expect(topLeft, const Offset(392.0, 346.0)); + expect(topRight, const Offset(463.0, 346.0)); + expect(bottomLeft, const Offset(392.0, 366.0)); + expect(bottomRight, const Offset(463.0, 366.0)); }); }