From 81a45ec2e5f80fa71d5135f1702ce540558b416d Mon Sep 17 00:00:00 2001 From: Christopher Fujino Date: Thu, 27 Aug 2020 14:14:33 -0700 Subject: [PATCH] Flutter 1.21.0-9.2.pre cherry picks (#64737) * update engine hash * allow null in compute for weak mode (#63515) * [Material] Relanding fix to ensure time picker input mode lays out correctly in RTL (#64097) * pin customer-testing * [flutter_tool] Handle Windows line endings in packages_test.dart (#63806) * [flutter_tool] Fix some create_test.dart tests on Windows (#63796) Co-authored-by: Alexandre Ardhuin Co-authored-by: Rami <2364772+rami-a@users.noreply.github.com> Co-authored-by: Zachary Anderson --- .cirrus.yml | 5 + bin/internal/engine.version | 2 +- .../lib/src/foundation/_isolates_io.dart | 2 +- .../flutter/lib/src/material/time_picker.dart | 164 ++++++++++++------ .../test/material/time_picker_test.dart | 10 ++ .../test/material/time_picker_test.dart | 76 +++++++- .../commands.shard/permeable/create_test.dart | 12 +- .../permeable/packages_test.dart | 13 +- 8 files changed, 222 insertions(+), 62 deletions(-) diff --git a/.cirrus.yml b/.cirrus.yml index c3020f3f126..ba7ca8f4510 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -277,6 +277,7 @@ task: script: - rm -rf bin/cache/pkg/tests - git clone https://github.com/flutter/tests.git bin/cache/pkg/tests + - (cd bin/cache/pkg/tests; git checkout c72bcde) - dart --enable-asserts dev/customer_testing/run_tests.dart --skip-on-fetch-failure --skip-template bin/cache/pkg/tests/registry/*.test # firebase_test_lab_tests are linux-only @@ -458,6 +459,9 @@ task: script: - CMD /S /C "IF EXIST "bin\cache\pkg\tests\" RMDIR /S /Q bin\cache\pkg\tests" - git clone https://github.com/flutter/tests.git bin\cache\pkg\tests + - cd bin\cache\pkg\tests + - git checkout c72bcde + - cd ..\..\..\.. - dart --enable-asserts dev\customer_testing\run_tests.dart --skip-on-fetch-failure --skip-template bin/cache/pkg/tests/registry/*.test # MACOS SHARDS @@ -568,6 +572,7 @@ task: - ulimit -S -n 2048 # https://github.com/flutter/flutter/issues/2976 - rm -rf bin/cache/pkg/tests - git clone https://github.com/flutter/tests.git bin/cache/pkg/tests + - (cd bin/cache/pkg/tests; git checkout c72bcde) - dart --enable-asserts dev/customer_testing/run_tests.dart --skip-on-fetch-failure --skip-template bin/cache/pkg/tests/registry/*.test - name: deploy_gallery-macos # linux- and macos- only diff --git a/bin/internal/engine.version b/bin/internal/engine.version index 52c36154bb7..803fb597b65 100644 --- a/bin/internal/engine.version +++ b/bin/internal/engine.version @@ -1 +1 @@ -267070c17a6956de1a03dbe09cda56f0c485f41b +20a953183580250aac2e15d36007664118bda5ab diff --git a/packages/flutter/lib/src/foundation/_isolates_io.dart b/packages/flutter/lib/src/foundation/_isolates_io.dart index 3760c439de1..1de7de10cc1 100644 --- a/packages/flutter/lib/src/foundation/_isolates_io.dart +++ b/packages/flutter/lib/src/foundation/_isolates_io.dart @@ -50,7 +50,7 @@ Future compute(isolates.ComputeCallback callback, Q message, { St } }); resultPort.listen((dynamic resultData) { - assert(resultData is R); + assert(resultData == null || resultData is R); if (!result.isCompleted) result.complete(resultData as R); }); diff --git a/packages/flutter/lib/src/material/time_picker.dart b/packages/flutter/lib/src/material/time_picker.dart index fc72ea320d0..8e1941a206a 100644 --- a/packages/flutter/lib/src/material/time_picker.dart +++ b/packages/flutter/lib/src/material/time_picker.dart @@ -1414,58 +1414,69 @@ class _TimePickerInputState extends State<_TimePickerInput> { ), const SizedBox(width: 12.0), ], - Expanded(child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox(height: 8.0), - _HourMinuteTextField( - selectedTime: _selectedTime, - isHour: true, - style: hourMinuteStyle, - validator: _validateHour, - onSavedSubmitted: _handleHourSavedSubmitted, - onChanged: _handleHourChanged, - ), - const SizedBox(height: 8.0), - if (!hourHasError && !minuteHasError) - ExcludeSemantics( - child: Text( - MaterialLocalizations.of(context).timePickerHourLabel, - style: theme.textTheme.caption, - maxLines: 1, - overflow: TextOverflow.ellipsis, + Expanded( + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + // Hour/minutes should not change positions in RTL locales. + textDirection: TextDirection.ltr, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 8.0), + _HourTextField( + selectedTime: _selectedTime, + style: hourMinuteStyle, + validator: _validateHour, + onSavedSubmitted: _handleHourSavedSubmitted, + onChanged: _handleHourChanged, + ), + const SizedBox(height: 8.0), + if (!hourHasError && !minuteHasError) + ExcludeSemantics( + child: Text( + MaterialLocalizations.of(context).timePickerHourLabel, + style: theme.textTheme.caption, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ], ), ), - ], - )), - Container( - margin: const EdgeInsets.only(top: 8.0), - height: _kTimePickerHeaderControlHeight, - child: _StringFragment(timeOfDayFormat: timeOfDayFormat), + Container( + margin: const EdgeInsets.only(top: 8.0), + height: _kTimePickerHeaderControlHeight, + child: _StringFragment(timeOfDayFormat: timeOfDayFormat), + ), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 8.0), + _MinuteTextField( + selectedTime: _selectedTime, + style: hourMinuteStyle, + validator: _validateMinute, + onSavedSubmitted: _handleMinuteSavedSubmitted, + ), + const SizedBox(height: 8.0), + if (!hourHasError && !minuteHasError) + ExcludeSemantics( + child: Text( + MaterialLocalizations.of(context).timePickerMinuteLabel, + style: theme.textTheme.caption, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ), + ], + ), ), - Expanded(child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox(height: 8.0), - _HourMinuteTextField( - selectedTime: _selectedTime, - isHour: false, - style: hourMinuteStyle, - validator: _validateMinute, - onSavedSubmitted: _handleMinuteSavedSubmitted, - ), - const SizedBox(height: 8.0), - if (!hourHasError && !minuteHasError) - ExcludeSemantics( - child: Text( - MaterialLocalizations.of(context).timePickerMinuteLabel, - style: theme.textTheme.caption, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ), - ], - )), if (!use24HourDials && timeOfDayFormat != TimeOfDayFormat.a_space_h_colon_mm) ...[ const SizedBox(width: 12.0), _DayPeriodControl( @@ -1489,6 +1500,61 @@ class _TimePickerInputState extends State<_TimePickerInput> { } } +class _HourTextField extends StatelessWidget { + const _HourTextField({ + Key key, + @required this.selectedTime, + @required this.style, + @required this.validator, + @required this.onSavedSubmitted, + @required this.onChanged, + }) : super(key: key); + + final TimeOfDay selectedTime; + final TextStyle style; + final FormFieldValidator validator; + final ValueChanged onSavedSubmitted; + final ValueChanged onChanged; + + @override + Widget build(BuildContext context) { + return _HourMinuteTextField( + selectedTime: selectedTime, + isHour: true, + style: style, + validator: validator, + onSavedSubmitted: onSavedSubmitted, + onChanged: onChanged, + ); + } +} + +class _MinuteTextField extends StatelessWidget { + const _MinuteTextField({ + Key key, + @required this.selectedTime, + @required this.style, + @required this.validator, + @required this.onSavedSubmitted, + }) : super(key: key); + + final TimeOfDay selectedTime; + final TextStyle style; + final FormFieldValidator validator; + final ValueChanged onSavedSubmitted; + + @override + Widget build(BuildContext context) { + return _HourMinuteTextField( + selectedTime: selectedTime, + isHour: false, + style: style, + validator: validator, + onSavedSubmitted: onSavedSubmitted, + ); + } +} + class _HourMinuteTextField extends StatefulWidget { const _HourMinuteTextField({ Key key, diff --git a/packages/flutter/test/material/time_picker_test.dart b/packages/flutter/test/material/time_picker_test.dart index 74e02113f76..945067f9eb4 100644 --- a/packages/flutter/test/material/time_picker_test.dart +++ b/packages/flutter/test/material/time_picker_test.dart @@ -806,6 +806,16 @@ void _testsInput() { await finishPicker(tester); expect(result, equals(const TimeOfDay(hour: 8, minute: 15))); }); + + // Fixes regression that was reverted in https://github.com/flutter/flutter/pull/64094#pullrequestreview-469836378. + testWidgets('Ensure hour/minute fields are top-aligned with the separator', (WidgetTester tester) async { + await startPicker(tester, (TimeOfDay time) { }, entryMode: TimePickerEntryMode.input); + final double hourFieldTop = tester.getTopLeft(find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_HourTextField')).dy; + final double minuteFieldTop = tester.getTopLeft(find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_MinuteTextField')).dy; + final double separatorTop = tester.getTopLeft(find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_StringFragment')).dy; + expect(hourFieldTop, separatorTop); + expect(minuteFieldTop, separatorTop); + }); } final Finder findDialPaint = find.descendant( diff --git a/packages/flutter_localizations/test/material/time_picker_test.dart b/packages/flutter_localizations/test/material/time_picker_test.dart index 870d06c9fbf..2d079941594 100644 --- a/packages/flutter_localizations/test/material/time_picker_test.dart +++ b/packages/flutter_localizations/test/material/time_picker_test.dart @@ -8,10 +8,16 @@ import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_test/flutter_test.dart'; class _TimePickerLauncher extends StatelessWidget { - const _TimePickerLauncher({ Key key, this.onChanged, this.locale }) : super(key: key); + const _TimePickerLauncher({ + Key key, + this.onChanged, + this.locale, + this.entryMode = TimePickerEntryMode.dial, + }) : super(key: key); final ValueChanged onChanged; final Locale locale; + final TimePickerEntryMode entryMode; @override Widget build(BuildContext context) { @@ -28,6 +34,7 @@ class _TimePickerLauncher extends StatelessWidget { onPressed: () async { onChanged(await showTimePicker( context: context, + initialEntryMode: entryMode, initialTime: const TimeOfDay(hour: 7, minute: 0), )); }, @@ -207,6 +214,73 @@ void main() { tester.binding.window.devicePixelRatioTestValue = null; }); + testWidgets('can localize input mode in all known formats', (WidgetTester tester) async { + final Finder stringFragmentTextFinder = find.descendant( + of: find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_StringFragment'), + matching: find.byType(Text), + ).first; + final Finder hourControlFinder = find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_HourTextField'); + final Finder minuteControlFinder = find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_MinuteTextField'); + final Finder dayPeriodControlFinder = find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_DayPeriodControl'); + + // TODO(yjbanov): also test `HH.mm` (in_ID), `a h:mm` (ko_KR) and `HH:mm น.` (th_TH) when we have .arb files for them + final List locales = [ + const Locale('en', 'US'), //'h:mm a' + const Locale('en', 'GB'), //'HH:mm' + const Locale('es', 'ES'), //'H:mm' + const Locale('fr', 'CA'), //'HH \'h\' mm' + const Locale('zh', 'ZH'), //'ah:mm' + const Locale('fa', 'IR'), //'H:mm' but RTL + ]; + + for (final Locale locale in locales) { + await tester.pumpWidget(_TimePickerLauncher(onChanged: (TimeOfDay time) { }, locale: locale, entryMode: TimePickerEntryMode.input)); + await tester.tap(find.text('X')); + await tester.pumpAndSettle(const Duration(seconds: 1)); + + final Text stringFragmentText = tester.widget(stringFragmentTextFinder); + final double hourLeftOffset = tester.getTopLeft(hourControlFinder).dx; + final double minuteLeftOffset = tester.getTopLeft(minuteControlFinder).dx; + final double stringFragmentLeftOffset = tester.getTopLeft(stringFragmentTextFinder).dx; + + if (locale == const Locale('en', 'US')) { + final double dayPeriodLeftOffset = tester.getTopLeft(dayPeriodControlFinder).dx; + expect(stringFragmentText.data, ':'); + expect(hourLeftOffset, lessThan(stringFragmentLeftOffset)); + expect(stringFragmentLeftOffset, lessThan(minuteLeftOffset)); + expect(minuteLeftOffset, lessThan(dayPeriodLeftOffset)); + } else if (locale == const Locale('en', 'GB')) { + expect(stringFragmentText.data, ':'); + expect(hourLeftOffset, lessThan(stringFragmentLeftOffset)); + expect(stringFragmentLeftOffset, lessThan(minuteLeftOffset)); + expect(dayPeriodControlFinder, findsNothing); + } else if (locale == const Locale('es', 'ES')) { + expect(stringFragmentText.data, ':'); + expect(hourLeftOffset, lessThan(stringFragmentLeftOffset)); + expect(stringFragmentLeftOffset, lessThan(minuteLeftOffset)); + expect(dayPeriodControlFinder, findsNothing); + } else if (locale == const Locale('fr', 'CA')) { + expect(stringFragmentText.data, 'h'); + expect(hourLeftOffset, lessThan(stringFragmentLeftOffset)); + expect(stringFragmentLeftOffset, lessThan(minuteLeftOffset)); + expect(dayPeriodControlFinder, findsNothing); + } else if (locale == const Locale('zh', 'ZH')) { + final double dayPeriodLeftOffset = tester.getTopLeft(dayPeriodControlFinder).dx; + expect(stringFragmentText.data, ':'); + expect(dayPeriodLeftOffset, lessThan(hourLeftOffset)); + expect(hourLeftOffset, lessThan(stringFragmentLeftOffset)); + expect(stringFragmentLeftOffset, lessThan(minuteLeftOffset)); + } else if (locale == const Locale('fa', 'IR')) { + // Even though this is an RTL locale, the hours and minutes positions should remain the same. + expect(stringFragmentText.data, ':'); + expect(hourLeftOffset, lessThan(stringFragmentLeftOffset)); + expect(stringFragmentLeftOffset, lessThan(minuteLeftOffset)); + expect(dayPeriodControlFinder, findsNothing); + } + await finishPicker(tester); + } + }); + testWidgets('uses single-ring 24-hour dial for all formats', (WidgetTester tester) async { const List locales = [ Locale('en', 'US'), // h diff --git a/packages/flutter_tools/test/commands.shard/permeable/create_test.dart b/packages/flutter_tools/test/commands.shard/permeable/create_test.dart index e872457c7bc..bd7f4c13b8c 100755 --- a/packages/flutter_tools/test/commands.shard/permeable/create_test.dart +++ b/packages/flutter_tools/test/commands.shard/permeable/create_test.dart @@ -1121,7 +1121,7 @@ void main() { await runner.run(['create', '--no-pub', projectDir.path]); final String metadata = globals.fs.file(globals.fs.path.join(projectDir.path, '.metadata')).readAsStringSync(); - expect(metadata, contains('project_type: app\n')); + expect(LineSplitter.split(metadata), contains('project_type: app')); }); testUsingContext('can re-gen default template over existing app project with no metadta and detect the type', () async { @@ -1138,7 +1138,7 @@ void main() { await runner.run(['create', '--no-pub', projectDir.path]); final String metadata = globals.fs.file(globals.fs.path.join(projectDir.path, '.metadata')).readAsStringSync(); - expect(metadata, contains('project_type: app\n')); + expect(LineSplitter.split(metadata), contains('project_type: app')); }); testUsingContext('can re-gen app template over existing app project and detect the type', () async { @@ -1152,7 +1152,7 @@ void main() { await runner.run(['create', '--no-pub', projectDir.path]); final String metadata = globals.fs.file(globals.fs.path.join(projectDir.path, '.metadata')).readAsStringSync(); - expect(metadata, contains('project_type: app\n')); + expect(LineSplitter.split(metadata), contains('project_type: app')); }); testUsingContext('can re-gen template over existing module project and detect the type', () async { @@ -1166,7 +1166,7 @@ void main() { await runner.run(['create', '--no-pub', projectDir.path]); final String metadata = globals.fs.file(globals.fs.path.join(projectDir.path, '.metadata')).readAsStringSync(); - expect(metadata, contains('project_type: module\n')); + expect(LineSplitter.split(metadata), contains('project_type: module')); }); testUsingContext('can re-gen default template over existing plugin project and detect the type', () async { @@ -1180,7 +1180,7 @@ void main() { await runner.run(['create', '--no-pub', projectDir.path]); final String metadata = globals.fs.file(globals.fs.path.join(projectDir.path, '.metadata')).readAsStringSync(); - expect(metadata, contains('project_type: plugin')); + expect(LineSplitter.split(metadata), contains('project_type: plugin')); }); testUsingContext('can re-gen default template over existing package project and detect the type', () async { @@ -1194,7 +1194,7 @@ void main() { await runner.run(['create', '--no-pub', projectDir.path]); final String metadata = globals.fs.file(globals.fs.path.join(projectDir.path, '.metadata')).readAsStringSync(); - expect(metadata, contains('project_type: package')); + expect(LineSplitter.split(metadata), contains('project_type: package')); }); testUsingContext('can re-gen module .android/ folder, reusing custom org', () async { diff --git a/packages/flutter_tools/test/commands.shard/permeable/packages_test.dart b/packages/flutter_tools/test/commands.shard/permeable/packages_test.dart index b4b08a85def..d2623ad5c49 100644 --- a/packages/flutter_tools/test/commands.shard/permeable/packages_test.dart +++ b/packages/flutter_tools/test/commands.shard/permeable/packages_test.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'dart:async'; +import 'dart:convert'; import 'package:args/command_runner.dart'; import 'package:flutter_tools/src/base/bot_detector.dart'; @@ -37,10 +38,14 @@ void main() { final String projectPath = await createProject(tempDir, arguments: arguments); final File pubspec = globals.fs.file(globals.fs.path.join(projectPath, 'pubspec.yaml')); String content = await pubspec.readAsString(); - content = content.replaceFirst( - '\ndependencies:\n', - '\ndependencies:\n $plugin:\n', - ); + final List contentLines = LineSplitter.split(content).toList(); + final int depsIndex = contentLines.indexOf('dependencies:'); + expect(depsIndex, isNot(-1)); + contentLines.replaceRange(depsIndex, depsIndex + 1, [ + 'dependencies:', + ' $plugin:', + ]); + content = contentLines.join('\n'); await pubspec.writeAsString(content, flush: true); return projectPath; }