diff --git a/dev/devicelab/bin/tasks/hello_world_impeller.dart b/dev/devicelab/bin/tasks/hello_world_impeller.dart index e67a25b48e6d39a06e1c151b9bdc843332e792fc..d094882fdaad1cfe23edc97219e0ff0665d35121 100644 --- a/dev/devicelab/bin/tasks/hello_world_impeller.dart +++ b/dev/devicelab/bin/tasks/hello_world_impeller.dart @@ -54,6 +54,7 @@ Future run() async { 'run', options: [ '--enable-impeller', + '--enable-vulkan-validation', '-d', device.deviceId, ], diff --git a/examples/api/lib/material/color_scheme/color_scheme.0.dart b/examples/api/lib/material/color_scheme/color_scheme.0.dart index 51ce183f9e2d8bbd2c5e3e8c074c17e62498fd58..78a6af44399057d1b38c1799b984e4ba63913dec 100644 --- a/examples/api/lib/material/color_scheme/color_scheme.0.dart +++ b/examples/api/lib/material/color_scheme/color_scheme.0.dart @@ -19,95 +19,93 @@ class ColorSchemeExample extends StatefulWidget { class _ColorSchemeExampleState extends State { Color selectedColor = ColorSeed.baseColor.color; + Brightness selectedBrightness = Brightness.light; + static const List schemeVariants = DynamicSchemeVariant.values; @override Widget build(BuildContext context) { - final Color? colorSeed = selectedColor == ColorSeed.baseColor.color ? null : selectedColor; - final ThemeData lightTheme = ThemeData( - colorSchemeSeed: colorSeed, - brightness: Brightness.light, - ); - final ThemeData darkTheme = ThemeData( - colorSchemeSeed: colorSeed, - brightness: Brightness.dark, - ); - - Widget schemeLabel(String brightness) { - return Padding( - padding: const EdgeInsets.symmetric(vertical: 15), - child: Text( - brightness, - style: const TextStyle(fontWeight: FontWeight.bold), - ), - ); - } - - Widget schemeView(ThemeData theme) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 15), - child: ColorSchemeView(colorScheme: theme.colorScheme), - ); - } - return MaterialApp( - theme: ThemeData(colorSchemeSeed: selectedColor), + debugShowCheckedModeBanner: false, + theme: ThemeData( + colorScheme: ColorScheme.fromSeed( + seedColor: selectedColor, + brightness: selectedBrightness, + ) + ), home: Builder( builder: (BuildContext context) => Scaffold( appBar: AppBar( title: const Text('ColorScheme'), - leading: MenuAnchor( - builder: (BuildContext context, MenuController controller, Widget? widget) { - return IconButton( - icon: Icon(Icons.circle, color: selectedColor), - onPressed: () { - setState(() { - if (!controller.isOpen) { - controller.open(); - } - }); - }, - ); - }, - menuChildren: List.generate(ColorSeed.values.length, (int index) { - final Color itemColor = ColorSeed.values[index].color; - return MenuItemButton( - leadingIcon: selectedColor == ColorSeed.values[index].color - ? Icon(Icons.circle, color: itemColor) - : Icon(Icons.circle_outlined, color: itemColor), - onPressed: () { - setState(() { - selectedColor = itemColor; - }); - }, - child: Text(ColorSeed.values[index].label), - ); - }), - ), + actions: [ + Row( + children: [ + const Text('Color Seed'), + MenuAnchor( + builder: (BuildContext context, MenuController controller, Widget? widget) { + return IconButton( + icon: Icon(Icons.circle, color: selectedColor), + onPressed: () { + setState(() { + if (!controller.isOpen) { + controller.open(); + } + }); + }, + ); + }, + menuChildren: List.generate(ColorSeed.values.length, (int index) { + final Color itemColor = ColorSeed.values[index].color; + return MenuItemButton( + leadingIcon: selectedColor == ColorSeed.values[index].color + ? Icon(Icons.circle, color: itemColor) + : Icon(Icons.circle_outlined, color: itemColor), + onPressed: () { + setState(() { + selectedColor = itemColor; + }); + }, + child: Text(ColorSeed.values[index].label), + ); + }), + ), + ], + ), + ], ), body: SingleChildScrollView( child: Padding( padding: const EdgeInsets.only(top: 5), child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row( - children: [ - Expanded( - child: Column( - children: [ - schemeLabel('Light ColorScheme'), - schemeView(lightTheme), - ], - ), - ), - Expanded( - child: Column( - children: [ - schemeLabel('Dark ColorScheme'), - schemeView(darkTheme), - ], + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24.0), + child: Row( + children: [ + const Text('Brightness'), + const SizedBox(width: 10), + Switch( + value: selectedBrightness == Brightness.light, + onChanged: (bool value) { + setState(() { + selectedBrightness = value ? Brightness.light : Brightness.dark; + }); + }, ), - ), - ], + ], + ), + ), + SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: List.generate(schemeVariants.length, (int index) { + return ColorSchemeVariantColumn( + selectedColor: selectedColor, + brightness: selectedBrightness, + schemeVariant: schemeVariants[index], + ); + }).toList(), + ), ), ], ), @@ -119,6 +117,47 @@ class _ColorSchemeExampleState extends State { } } +class ColorSchemeVariantColumn extends StatelessWidget { + const ColorSchemeVariantColumn({ + super.key, + this.schemeVariant = DynamicSchemeVariant.tonalSpot, + this.brightness = Brightness.light, + required this.selectedColor, + }); + + final DynamicSchemeVariant schemeVariant; + final Brightness brightness; + final Color selectedColor; + + @override + Widget build(BuildContext context) { + return ConstrainedBox( + constraints: const BoxConstraints.tightFor(width: 250), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 15), + child: Text( + schemeVariant.name == 'tonalSpot' ? '${schemeVariant.name} (Default)' : schemeVariant.name, + style: const TextStyle(fontWeight: FontWeight.bold), + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 15), + child: ColorSchemeView( + colorScheme: ColorScheme.fromSeed( + seedColor: selectedColor, + brightness: brightness, + dynamicSchemeVariant: schemeVariant, + ), + ), + ), + ], + ) + ); + } +} + class ColorSchemeView extends StatelessWidget { const ColorSchemeView({super.key, required this.colorScheme}); @@ -301,7 +340,10 @@ enum ColorSeed { yellow('Yellow', Colors.yellow), orange('Orange', Colors.orange), deepOrange('Deep Orange', Colors.deepOrange), - pink('Pink', Colors.pink); + pink('Pink', Colors.pink), + brightBlue('Bright Blue', Color(0xFF0000FF)), + brightGreen('Bright Green', Color(0xFF00FF00)), + brightRed('Bright Red', Color(0xFFFF0000)); const ColorSeed(this.label, this.color); final String label; diff --git a/examples/api/test/material/color_scheme/color_scheme.0_test.dart b/examples/api/test/material/color_scheme/color_scheme.0_test.dart index 492a71884b926de2b1b82c4856c9e640cdf5c8f0..58e0e142ff097c6faa92bc6ae2d7f1c47eec35a4 100644 --- a/examples/api/test/material/color_scheme/color_scheme.0_test.dart +++ b/examples/api/test/material/color_scheme/color_scheme.0_test.dart @@ -11,10 +11,9 @@ void main() { await tester.pumpWidget( const example.ColorSchemeExample(), ); - expect(find.text('Light ColorScheme'), findsOneWidget); - expect(find.text('Dark ColorScheme'), findsOneWidget); + expect(find.text('tonalSpot (Default)'), findsOneWidget); - expect(find.byType(example.ColorChip), findsNWidgets(86)); + expect(find.byType(example.ColorChip), findsNWidgets(43 * 9)); }); testWidgets('Change color seed', (WidgetTester tester) async { @@ -30,7 +29,7 @@ void main() { ) ); } - expect(coloredBox().color, const Color(0xFF6750A4)); + expect(coloredBox().color, const Color(0xff65558f)); await tester.tap(find.byType(MenuAnchor)); await tester.pumpAndSettle(); await tester.tap(find.widgetWithText(MenuItemButton, 'Yellow')); diff --git a/examples/api/test/widgets/transitions/listenable_builder.1_test.dart b/examples/api/test/widgets/transitions/listenable_builder.1_test.dart index 00f18d1c28b12fec9c68b64f7b08df05d11a4831..9b33fb6f021f100eac96ced21dcddd57058c92b0 100644 --- a/examples/api/test/widgets/transitions/listenable_builder.1_test.dart +++ b/examples/api/test/widgets/transitions/listenable_builder.1_test.dart @@ -10,7 +10,7 @@ void main() { testWidgets('Tapping FAB increments counter', (WidgetTester tester) async { await tester.pumpWidget(const example.ListenableBuilderExample()); - String getCount() => (tester.widget(find.descendant(of: find.byType(ListenableBuilder), matching: find.byType(Text))) as Text).data!; + String getCount() => (tester.widget(find.descendant(of: find.byType(ListenableBuilder).last, matching: find.byType(Text))) as Text).data!; expect(find.text('Current counter value:'), findsOneWidget); expect(find.text('0'), findsOneWidget); diff --git a/packages/flutter/lib/src/material/color_scheme.dart b/packages/flutter/lib/src/material/color_scheme.dart index eaa01ace6ba1f381651015a6a0b7bb707411f9eb..76387afbb02d482c3c07e70918941d0a84017932 100644 --- a/packages/flutter/lib/src/material/color_scheme.dart +++ b/packages/flutter/lib/src/material/color_scheme.dart @@ -8,10 +8,58 @@ import 'dart:ui' as ui; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:material_color_utilities/material_color_utilities.dart'; +import 'package:material_color_utilities/scheme/scheme_fruit_salad.dart'; +import 'package:material_color_utilities/scheme/scheme_rainbow.dart'; import 'colors.dart'; import 'theme_data.dart'; +/// The algorithm used to construct a [ColorScheme] in [ColorScheme.fromSeed]. +/// +/// The `tonalSpot` variant builds default Material scheme colors. These colors are +/// mapped to light or dark tones to achieve visually accessible color +/// pairings with sufficient contrast between foreground and background elements. +/// +/// In some cases, the tones can prevent colors from appearing as intended, +/// such as when a color is too light to offer enough contrast for accessibility. +/// Color fidelity (`DynamicSchemeVariant.fidelity`) is a feature that adjusts +/// tones in these cases to produce the intended visual results without harming +/// visual contrast. +enum DynamicSchemeVariant { + /// Default for Material theme colors. Builds pastel palettes with a low chroma. + tonalSpot, + + /// The resulting color palettes match seed color, even if the seed color + /// is very bright (high chroma). + fidelity, + + /// All colors are grayscale, no chroma. + monochrome, + + /// Close to grayscale, a hint of chroma. + neutral, + + /// Pastel colors, high chroma palettes. The primary palette's chroma is at + /// maximum. Use `fidelity` instead if tokens should alter their tone to match + /// the palette vibrancy. + vibrant, + + /// Pastel colors, medium chroma palettes. The primary palette's hue is + /// different from the seed color, for variety. + expressive, + + /// Almost identical to `fidelity`. Tokens and palettes match the seed color. + /// [ColorScheme.primaryContainer] is the seed color, adjusted to ensure + /// contrast with surfaces. The tertiary palette is analogue of the seed color. + content, + + /// A playful theme - the seed color's hue does not appear in the theme. + rainbow, + + /// A playful theme - the seed color's hue does not appear in the theme. + fruitSalad, +} + /// {@template flutter.material.color_scheme.ColorScheme} /// A set of 45 colors based on the /// [Material spec](https://m3.material.io/styles/color/the-color-system/color-roles) @@ -215,20 +263,35 @@ class ColorScheme with Diagnosticable { /// Generate a [ColorScheme] derived from the given `seedColor`. /// - /// Using the seedColor as a starting point, a set of tonal palettes are - /// constructed. These tonal palettes are based on the Material 3 Color - /// system and provide all the needed colors for a [ColorScheme]. These - /// colors are designed to work well together and meet contrast - /// requirements for accessibility. + /// Using the `seedColor` as a starting point, a set of tonal palettes are + /// constructed. By default, the tonal palettes are based on the Material 3 + /// Color system and provide all of the [ColorScheme] colors. These colors are + /// designed to work well together and meet contrast requirements for + /// accessibility. /// /// If any of the optional color parameters are non-null they will be /// used in place of the generated colors for that field in the resulting /// color scheme. This allows apps to override specific colors for their /// needs. /// - /// Given the nature of the algorithm, the seedColor may not wind up as + /// Given the nature of the algorithm, the `seedColor` may not wind up as /// one of the ColorScheme colors. /// + /// The `dynamicSchemeVariant` parameter creates different types of + /// [DynamicScheme]s, which are used to generate different styles of [ColorScheme]s. + /// By default, `dynamicSchemeVariant` is set to `tonalSpot`. A [ColorScheme] + /// constructed by `dynamicSchemeVariant.tonalSpot` has pastel palettes and + /// won't be too "colorful" even if the `seedColor` has a high chroma value. + /// If the resulting color scheme is too dark, consider setting `dynamicSchemeVariant` + /// to [DynamicSchemeVariant.fidelity], whose palettes match the seed color. + /// + /// {@tool dartpad} + /// This sample shows how to use [ColorScheme.fromSeed] to create dynamic + /// color schemes with different [DynamicSchemeVariant]s. + /// + /// ** See code in examples/api/lib/material/color_scheme/color_scheme.0.dart ** + /// {@end-tool} + /// /// See also: /// /// * , the @@ -238,6 +301,7 @@ class ColorScheme with Diagnosticable { factory ColorScheme.fromSeed({ required Color seedColor, Brightness brightness = Brightness.light, + DynamicSchemeVariant dynamicSchemeVariant = DynamicSchemeVariant.tonalSpot, Color? primary, Color? onPrimary, Color? primaryContainer, @@ -300,13 +364,7 @@ class ColorScheme with Diagnosticable { ) Color? surfaceVariant, }) { - final SchemeTonalSpot scheme; - switch (brightness) { - case Brightness.light: - scheme = SchemeTonalSpot(sourceColorHct: Hct.fromInt(seedColor.value), isDark: false, contrastLevel: 0.0); - case Brightness.dark: - scheme = SchemeTonalSpot(sourceColorHct: Hct.fromInt(seedColor.value), isDark: true, contrastLevel: 0.0); - } + final DynamicScheme scheme = _buildDynamicScheme(brightness, seedColor, dynamicSchemeVariant); return ColorScheme( primary: primary ?? Color(MaterialDynamicColors.primary.getArgb(scheme)), @@ -1615,6 +1673,7 @@ class ColorScheme with Diagnosticable { static Future fromImageProvider({ required ImageProvider provider, Brightness brightness = Brightness.light, + DynamicSchemeVariant dynamicSchemeVariant = DynamicSchemeVariant.tonalSpot, Color? primary, Color? onPrimary, Color? primaryContainer, @@ -1688,13 +1747,7 @@ class ColorScheme with Diagnosticable { final List scoredResults = Score.score(colorToCount, desired: 1); final ui.Color baseColor = Color(scoredResults.first); - final SchemeTonalSpot scheme; - switch (brightness) { - case Brightness.light: - scheme = SchemeTonalSpot(sourceColorHct: Hct.fromInt(baseColor.value), isDark: false, contrastLevel: 0.0); - case Brightness.dark: - scheme = SchemeTonalSpot(sourceColorHct: Hct.fromInt(baseColor.value), isDark: true, contrastLevel: 0.0); - } + final DynamicScheme scheme = _buildDynamicScheme(brightness, baseColor, dynamicSchemeVariant); return ColorScheme( primary: primary ?? Color(MaterialDynamicColors.primary.getArgb(scheme)), @@ -1830,4 +1883,20 @@ class ColorScheme with Diagnosticable { final int b = abgr & onlyBMask; return (abgr & exceptRMask & exceptBMask) | (b << 16) | r; } + + static DynamicScheme _buildDynamicScheme(Brightness brightness, Color seedColor, DynamicSchemeVariant schemeVariant) { + final bool isDark = brightness == Brightness.dark; + final Hct sourceColor = Hct.fromInt(seedColor.value); + return switch (schemeVariant) { + DynamicSchemeVariant.tonalSpot => SchemeTonalSpot(sourceColorHct: sourceColor, isDark: isDark, contrastLevel: 0.0), + DynamicSchemeVariant.fidelity => SchemeFidelity(sourceColorHct: sourceColor, isDark: isDark, contrastLevel: 0.0), + DynamicSchemeVariant.content => SchemeContent(sourceColorHct: sourceColor, isDark: isDark, contrastLevel: 0.0), + DynamicSchemeVariant.monochrome => SchemeMonochrome(sourceColorHct: sourceColor, isDark: isDark, contrastLevel: 0.0), + DynamicSchemeVariant.neutral => SchemeNeutral(sourceColorHct: sourceColor, isDark: isDark, contrastLevel: 0.0), + DynamicSchemeVariant.vibrant => SchemeVibrant(sourceColorHct: sourceColor, isDark: isDark, contrastLevel: 0.0), + DynamicSchemeVariant.expressive => SchemeExpressive(sourceColorHct: sourceColor, isDark: isDark, contrastLevel: 0.0), + DynamicSchemeVariant.rainbow => SchemeRainbow(sourceColorHct: sourceColor, isDark: isDark, contrastLevel: 0.0), + DynamicSchemeVariant.fruitSalad => SchemeFruitSalad(sourceColorHct: sourceColor, isDark: isDark, contrastLevel: 0.0), + }; + } } diff --git a/packages/flutter/lib/src/widgets/default_text_editing_shortcuts.dart b/packages/flutter/lib/src/widgets/default_text_editing_shortcuts.dart index 3ffc0d2c83aeff318ffdf7289d0be33d46d8f0ae..e9890a1a751cdefee9969da12a55b5135797627e 100644 --- a/packages/flutter/lib/src/widgets/default_text_editing_shortcuts.dart +++ b/packages/flutter/lib/src/widgets/default_text_editing_shortcuts.dart @@ -532,7 +532,21 @@ class DefaultTextEditingShortcuts extends StatelessWidget { Map? _getDisablingShortcut() { if (kIsWeb) { - return _webDisablingTextShortcuts; + switch (defaultTargetPlatform) { + case TargetPlatform.linux: + return { + ..._webDisablingTextShortcuts, + for (final ShortcutActivator activator in _linuxNumpadShortcuts.keys) + activator as SingleActivator: const DoNothingAndStopPropagationTextIntent(), + }; + case TargetPlatform.android: + case TargetPlatform.fuchsia: + case TargetPlatform.windows: + case TargetPlatform.iOS: + case TargetPlatform.macOS: + case TargetPlatform.ohos: + return _webDisablingTextShortcuts; + } } switch (defaultTargetPlatform) { case TargetPlatform.android: diff --git a/packages/flutter/lib/src/widgets/editable_text.dart b/packages/flutter/lib/src/widgets/editable_text.dart index 6d2e749c97e76234b1efbbfdae917c89075d2836..bfc41d990eead16ad86758e0869786b98a111d77 100644 --- a/packages/flutter/lib/src/widgets/editable_text.dart +++ b/packages/flutter/lib/src/widgets/editable_text.dart @@ -311,7 +311,7 @@ class TextEditingController extends ValueNotifier { /// If the new selection is outside the composing range, the composing range is /// cleared. set selection(TextSelection newSelection) { - if (!_isSelectionWithinTextBounds(newSelection)) { + if (text.length < newSelection.end || text.length < newSelection.start) { throw FlutterError('invalid text selection: $newSelection'); } final TextRange newComposing = _isSelectionWithinComposingRange(newSelection) ? value.composing : TextRange.empty; @@ -345,11 +345,6 @@ class TextEditingController extends ValueNotifier { value = value.copyWith(composing: TextRange.empty); } - /// Check that the [selection] is inside of the bounds of [text]. - bool _isSelectionWithinTextBounds(TextSelection selection) { - return selection.start <= text.length && selection.end <= text.length; - } - /// Check that the [selection] is inside of the composing range. bool _isSelectionWithinComposingRange(TextSelection selection) { return selection.start >= value.composing.start && selection.end <= value.composing.end; @@ -3938,7 +3933,8 @@ class EditableTextState extends State with AutomaticKeepAliveClien // We return early if the selection is not valid. This can happen when the // text of [EditableText] is updated at the same time as the selection is // changed by a gesture event. - if (!widget.controller._isSelectionWithinTextBounds(selection)) { + final String text = widget.controller.value.text; + if (text.length < selection.end || text.length < selection.start) { return; } diff --git a/packages/flutter/lib/src/widgets/routes.dart b/packages/flutter/lib/src/widgets/routes.dart index 277a5fb6c553da2e460c0d0b351ed180a53fcccc..890d320e935592e20ac744eca87afe7a503d868a 100644 --- a/packages/flutter/lib/src/widgets/routes.dart +++ b/packages/flutter/lib/src/widgets/routes.dart @@ -1022,6 +1022,8 @@ class _ModalScopeState extends State<_ModalScope> { @override Widget build(BuildContext context) { + // Only top most route can participate in focus traversal. + focusScopeNode.skipTraversal = !widget.route.isCurrent; return AnimatedBuilder( animation: widget.route.restorationScopeId, builder: (BuildContext context, Widget? child) { @@ -1048,24 +1050,22 @@ class _ModalScopeState extends State<_ModalScope> { }, child: PrimaryScrollController( controller: primaryScrollController, - child: FocusScope( - node: focusScopeNode, // immutable - // Only top most route can participate in focus traversal. - skipTraversal: !widget.route.isCurrent, + child: FocusScope.withExternalFocusNode( + focusScopeNode: focusScopeNode, // immutable child: RepaintBoundary( - child: AnimatedBuilder( - animation: _listenable, // immutable + child: ListenableBuilder( + listenable: _listenable, // immutable builder: (BuildContext context, Widget? child) { return widget.route.buildTransitions( context, widget.route.animation!, widget.route.secondaryAnimation!, - // This additional AnimatedBuilder is include because if the + // This additional ListenableBuilder is include because if the // value of the userGestureInProgressNotifier changes, it's // only necessary to rebuild the IgnorePointer widget and set // the focus node's ability to focus. - AnimatedBuilder( - animation: widget.route.navigator?.userGestureInProgressNotifier ?? ValueNotifier(false), + ListenableBuilder( + listenable: widget.route.navigator?.userGestureInProgressNotifier ?? ValueNotifier(false), builder: (BuildContext context, Widget? child) { final bool ignoreEvents = _shouldIgnoreFocusRequest; focusScopeNode.canRequestFocus = !ignoreEvents; diff --git a/packages/flutter/lib/src/widgets/service_extensions.dart b/packages/flutter/lib/src/widgets/service_extensions.dart index caeb190758ac067c9dd1f456b4d2ea5f45f1fe61..54971b59c386423967126d33936186a03b8ddbee 100644 --- a/packages/flutter/lib/src/widgets/service_extensions.dart +++ b/packages/flutter/lib/src/widgets/service_extensions.dart @@ -144,7 +144,22 @@ enum WidgetInspectorServiceExtensions { /// extension is registered. trackRebuildDirtyWidgets, + /// Name of service extension that, when called, returns the mapping of + /// widget locations to ids. + /// + /// This service extension is only supported if + /// [WidgetInspectorService._widgetCreationTracked] is true. + /// + /// See also: + /// + /// * [trackRebuildDirtyWidgets], which toggles dispatching events that use + /// these ids to efficiently indicate the locations of widgets. + /// * [WidgetInspectorService.initServiceExtensions], where the service + /// extension is registered. + widgetLocationIdMap, + /// Name of service extension that, when called, determines whether + /// [WidgetInspectorService._trackRepaintWidgets], which determines whether /// a callback is invoked for every [RenderObject] painted each frame. /// /// See also: diff --git a/packages/flutter/lib/src/widgets/two_dimensional_viewport.dart b/packages/flutter/lib/src/widgets/two_dimensional_viewport.dart index 4bc518129c955aed4904eb8e9b605f57b2634668..b10a9ca437aec1c22c4226126a9b980bacdc2056 100644 --- a/packages/flutter/lib/src/widgets/two_dimensional_viewport.dart +++ b/packages/flutter/lib/src/widgets/two_dimensional_viewport.dart @@ -1647,6 +1647,10 @@ abstract class RenderTwoDimensionalViewport extends RenderBox implements RenderA _children.remove(slot); } assert(_debugTrackOrphans(noLongerOrphan: child)); + if (_keepAliveBucket[childParentData.vicinity] == child) { + _keepAliveBucket.remove(childParentData.vicinity); + } + assert(_keepAliveBucket[childParentData.vicinity] != child); dropChild(child); return; } diff --git a/packages/flutter/lib/src/widgets/widget_inspector.dart b/packages/flutter/lib/src/widgets/widget_inspector.dart index d578e594cb858f6aa48dcc20d948955ea71aba4a..9ebcf2229746926d9db5b2150aada76a7ea354ce 100644 --- a/packages/flutter/lib/src/widgets/widget_inspector.dart +++ b/packages/flutter/lib/src/widgets/widget_inspector.dart @@ -1120,6 +1120,14 @@ mixin WidgetInspectorService { registerExtension: registerExtension, ); + _registerSignalServiceExtension( + name: WidgetInspectorServiceExtensions.widgetLocationIdMap.name, + callback: () { + return _locationIdMapToJson(); + }, + registerExtension: registerExtension, + ); + _registerBoolServiceExtension( name: WidgetInspectorServiceExtensions.trackRepaintWidgets.name, getter: () async => _trackRepaintWidgets, @@ -2375,9 +2383,11 @@ mixin WidgetInspectorService { bool? _widgetCreationTracked; late Duration _frameStart; + late int _frameNumber; void _onFrameStart(Duration timeStamp) { _frameStart = timeStamp; + _frameNumber = PlatformDispatcher.instance.frameData.frameNumber; SchedulerBinding.instance.addPostFrameCallback(_onFrameEnd, debugLabel: 'WidgetInspector.onFrameStart'); } @@ -2391,7 +2401,13 @@ mixin WidgetInspectorService { } void _postStatsEvent(String eventName, _ElementLocationStatsTracker stats) { - postEvent(eventName, stats.exportToJson(_frameStart)); + postEvent( + eventName, + stats.exportToJson( + _frameStart, + frameNumber: _frameNumber, + ), + ); } /// All events dispatched by a [WidgetInspectorService] use this method @@ -2600,7 +2616,7 @@ class _ElementLocationStatsTracker { /// Exports the current counts and then resets the stats to prepare to track /// the next frame of data. - Map exportToJson(Duration startTime) { + Map exportToJson(Duration startTime, {required int frameNumber}) { final List events = List.filled(active.length * 2, 0); int j = 0; for (final _LocationCount stat in active) { @@ -2610,6 +2626,7 @@ class _ElementLocationStatsTracker { final Map json = { 'startTime': startTime.inMicroseconds, + 'frameNumber': frameNumber, 'events': events, }; @@ -3256,12 +3273,21 @@ class _InspectorOverlayLayer extends Layer { final Rect targetRect = MatrixUtils.transformRect( state.selected.transform, state.selected.rect, ); - final Offset target = Offset(targetRect.left, targetRect.center.dy); - const double offsetFromWidget = 9.0; - final double verticalOffset = (targetRect.height) / 2 + offsetFromWidget; - - _paintDescription(canvas, state.tooltip, state.textDirection, target, verticalOffset, size, targetRect); - + if (!targetRect.hasNaN) { + final Offset target = Offset(targetRect.left, targetRect.center.dy); + const double offsetFromWidget = 9.0; + final double verticalOffset = (targetRect.height) / 2 + offsetFromWidget; + + _paintDescription( + canvas, + state.tooltip, + state.textDirection, + target, + verticalOffset, + size, + targetRect, + ); + } // TODO(jacobr): provide an option to perform a debug paint of just the // selected widget. return recorder.endRecording(); @@ -3651,6 +3677,34 @@ int _toLocationId(_Location location) { return id; } +Map _locationIdMapToJson() { + const String idsKey = 'ids'; + const String linesKey = 'lines'; + const String columnsKey = 'columns'; + const String namesKey = 'names'; + + final Map>> fileLocationsMap = + >>{}; + for (final MapEntry<_Location, int> entry in _locationToId.entries) { + final _Location location = entry.key; + final Map> locations = fileLocationsMap.putIfAbsent( + location.file, + () => >{ + idsKey: [], + linesKey: [], + columnsKey: [], + namesKey: [], + }, + ); + + locations[idsKey]!.add(entry.value); + locations[linesKey]!.add(location.line); + locations[columnsKey]!.add(location.column); + locations[namesKey]!.add(location.name); + } + return fileLocationsMap; +} + /// A delegate that configures how a hierarchy of [DiagnosticsNode]s are /// serialized by the Flutter Inspector. @visibleForTesting diff --git a/packages/flutter/test/foundation/service_extensions_test.dart b/packages/flutter/test/foundation/service_extensions_test.dart index 14db86f397fa85e87e9e529da6b35b79ffd5a795..b0c3ba6541e45d069b783fce26e35711ce70071e 100644 --- a/packages/flutter/test/foundation/service_extensions_test.dart +++ b/packages/flutter/test/foundation/service_extensions_test.dart @@ -170,7 +170,7 @@ void main() { if (WidgetInspectorService.instance.isWidgetCreationTracked()) { // Some inspector extensions are only exposed if widget creation locations // are tracked. - widgetInspectorExtensionCount += 2; + widgetInspectorExtensionCount += 3; } expect(binding.extensions.keys.where((String name) => name.startsWith('inspector.')), hasLength(widgetInspectorExtensionCount)); diff --git a/packages/flutter/test/material/color_scheme_test.dart b/packages/flutter/test/material/color_scheme_test.dart index 17eba66e0e756d02e9785fa843968589b1addd4b..36df995aac00b215422ccb07b4aa737d6041b281 100644 --- a/packages/flutter/test/material/color_scheme_test.dart +++ b/packages/flutter/test/material/color_scheme_test.dart @@ -2,10 +2,13 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:typed_data'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:material_color_utilities/material_color_utilities.dart'; +import 'package:material_color_utilities/scheme/scheme_fruit_salad.dart'; +import 'package:material_color_utilities/scheme/scheme_rainbow.dart'; import '../image_data.dart'; @@ -684,4 +687,143 @@ void main() { }, skip: isBrowser, // https://github.com/flutter/flutter/issues/44115 ); + + testWidgets('Color values in ColorScheme.fromSeed with different variants matches values in DynamicScheme', (WidgetTester tester) async { + const Color seedColor = Colors.orange; + final Hct sourceColor = Hct.fromInt(seedColor.value); + for (final DynamicSchemeVariant schemeVariant in DynamicSchemeVariant.values) { + final DynamicScheme dynamicScheme = switch (schemeVariant) { + DynamicSchemeVariant.tonalSpot => SchemeTonalSpot(sourceColorHct: sourceColor, isDark: false, contrastLevel: 0.0), + DynamicSchemeVariant.fidelity => SchemeFidelity(sourceColorHct: sourceColor, isDark: false, contrastLevel: 0.0), + DynamicSchemeVariant.content => SchemeContent(sourceColorHct: sourceColor, isDark: false, contrastLevel: 0.0), + DynamicSchemeVariant.monochrome => SchemeMonochrome(sourceColorHct: sourceColor, isDark: false, contrastLevel: 0.0), + DynamicSchemeVariant.neutral => SchemeNeutral(sourceColorHct: sourceColor, isDark: false, contrastLevel: 0.0), + DynamicSchemeVariant.vibrant => SchemeVibrant(sourceColorHct: sourceColor, isDark: false, contrastLevel: 0.0), + DynamicSchemeVariant.expressive => SchemeExpressive(sourceColorHct: sourceColor, isDark: false, contrastLevel: 0.0), + DynamicSchemeVariant.rainbow => SchemeRainbow(sourceColorHct: sourceColor, isDark: false, contrastLevel: 0.0), + DynamicSchemeVariant.fruitSalad => SchemeFruitSalad(sourceColorHct: sourceColor, isDark: false, contrastLevel: 0.0), + }; + final ColorScheme colorScheme = ColorScheme.fromSeed( + seedColor: seedColor, + dynamicSchemeVariant: schemeVariant, + ); + + expect(colorScheme.primary.value, MaterialDynamicColors.primary.getArgb(dynamicScheme)); + expect(colorScheme.onPrimary.value, MaterialDynamicColors.onPrimary.getArgb(dynamicScheme)); + expect(colorScheme.primaryContainer.value, MaterialDynamicColors.primaryContainer.getArgb(dynamicScheme)); + expect(colorScheme.onPrimaryContainer.value, MaterialDynamicColors.onPrimaryContainer.getArgb(dynamicScheme)); + expect(colorScheme.primaryFixed.value, MaterialDynamicColors.primaryFixed.getArgb(dynamicScheme)); + expect(colorScheme.primaryFixedDim.value, MaterialDynamicColors.primaryFixedDim.getArgb(dynamicScheme)); + expect(colorScheme.onPrimaryFixed.value, MaterialDynamicColors.onPrimaryFixed.getArgb(dynamicScheme)); + expect(colorScheme.onPrimaryFixedVariant.value, MaterialDynamicColors.onPrimaryFixedVariant.getArgb(dynamicScheme)); + expect(colorScheme.secondary.value, MaterialDynamicColors.secondary.getArgb(dynamicScheme)); + expect(colorScheme.onSecondary.value, MaterialDynamicColors.onSecondary.getArgb(dynamicScheme)); + expect(colorScheme.secondaryContainer.value, MaterialDynamicColors.secondaryContainer.getArgb(dynamicScheme)); + expect(colorScheme.onSecondaryContainer.value, MaterialDynamicColors.onSecondaryContainer.getArgb(dynamicScheme)); + expect(colorScheme.secondaryFixed.value, MaterialDynamicColors.secondaryFixed.getArgb(dynamicScheme)); + expect(colorScheme.secondaryFixedDim.value, MaterialDynamicColors.secondaryFixedDim.getArgb(dynamicScheme)); + expect(colorScheme.onSecondaryFixed.value, MaterialDynamicColors.onSecondaryFixed.getArgb(dynamicScheme)); + expect(colorScheme.onSecondaryFixedVariant.value, MaterialDynamicColors.onSecondaryFixedVariant.getArgb(dynamicScheme)); + expect(colorScheme.tertiary.value, MaterialDynamicColors.tertiary.getArgb(dynamicScheme)); + expect(colorScheme.onTertiary.value, MaterialDynamicColors.onTertiary.getArgb(dynamicScheme)); + expect(colorScheme.tertiaryContainer.value, MaterialDynamicColors.tertiaryContainer.getArgb(dynamicScheme)); + expect(colorScheme.onTertiaryContainer.value, MaterialDynamicColors.onTertiaryContainer.getArgb(dynamicScheme)); + expect(colorScheme.tertiaryFixed.value, MaterialDynamicColors.tertiaryFixed.getArgb(dynamicScheme)); + expect(colorScheme.tertiaryFixedDim.value, MaterialDynamicColors.tertiaryFixedDim.getArgb(dynamicScheme)); + expect(colorScheme.onTertiaryFixed.value, MaterialDynamicColors.onTertiaryFixed.getArgb(dynamicScheme)); + expect(colorScheme.onTertiaryFixedVariant.value, MaterialDynamicColors.onTertiaryFixedVariant.getArgb(dynamicScheme)); + expect(colorScheme.error.value, MaterialDynamicColors.error.getArgb(dynamicScheme)); + expect(colorScheme.onError.value, MaterialDynamicColors.onError.getArgb(dynamicScheme)); + expect(colorScheme.errorContainer.value, MaterialDynamicColors.errorContainer.getArgb(dynamicScheme)); + expect(colorScheme.onErrorContainer.value, MaterialDynamicColors.onErrorContainer.getArgb(dynamicScheme)); + expect(colorScheme.background.value, MaterialDynamicColors.background.getArgb(dynamicScheme)); + expect(colorScheme.onBackground.value, MaterialDynamicColors.onBackground.getArgb(dynamicScheme)); + expect(colorScheme.surface.value, MaterialDynamicColors.surface.getArgb(dynamicScheme)); + expect(colorScheme.surfaceDim.value, MaterialDynamicColors.surfaceDim.getArgb(dynamicScheme)); + expect(colorScheme.surfaceBright.value, MaterialDynamicColors.surfaceBright.getArgb(dynamicScheme)); + expect(colorScheme.surfaceContainerLowest.value, MaterialDynamicColors.surfaceContainerLowest.getArgb(dynamicScheme)); + expect(colorScheme.surfaceContainerLow.value, MaterialDynamicColors.surfaceContainerLow.getArgb(dynamicScheme)); + expect(colorScheme.surfaceContainer.value, MaterialDynamicColors.surfaceContainer.getArgb(dynamicScheme)); + expect(colorScheme.surfaceContainerHigh.value, MaterialDynamicColors.surfaceContainerHigh.getArgb(dynamicScheme)); + expect(colorScheme.surfaceContainerHighest.value, MaterialDynamicColors.surfaceContainerHighest.getArgb(dynamicScheme)); + expect(colorScheme.onSurface.value, MaterialDynamicColors.onSurface.getArgb(dynamicScheme)); + expect(colorScheme.surfaceVariant.value, MaterialDynamicColors.surfaceVariant.getArgb(dynamicScheme)); + expect(colorScheme.onSurfaceVariant.value, MaterialDynamicColors.onSurfaceVariant.getArgb(dynamicScheme)); + expect(colorScheme.outline.value, MaterialDynamicColors.outline.getArgb(dynamicScheme)); + expect(colorScheme.outlineVariant.value, MaterialDynamicColors.outlineVariant.getArgb(dynamicScheme)); + expect(colorScheme.shadow.value, MaterialDynamicColors.shadow.getArgb(dynamicScheme)); + expect(colorScheme.scrim.value, MaterialDynamicColors.scrim.getArgb(dynamicScheme)); + expect(colorScheme.inverseSurface.value, MaterialDynamicColors.inverseSurface.getArgb(dynamicScheme)); + expect(colorScheme.onInverseSurface.value, MaterialDynamicColors.inverseOnSurface.getArgb(dynamicScheme)); + expect(colorScheme.inversePrimary.value, MaterialDynamicColors.inversePrimary.getArgb(dynamicScheme)); + } + }); + + testWidgets('ColorScheme.fromSeed with different variants spot checks', (WidgetTester tester) async { + // Default (Variant.tonalSpot). + await _testFilledButtonColor(tester, ColorScheme.fromSeed(seedColor: const Color(0xFF000000)), const Color(0xFF8C4A60)); + await _testFilledButtonColor(tester, ColorScheme.fromSeed(seedColor: const Color(0xFF00FF00)), const Color(0xFF406836)); + await _testFilledButtonColor(tester, ColorScheme.fromSeed(seedColor: const Color(0xFF6559F5)), const Color(0xFF5B5891)); + await _testFilledButtonColor(tester, ColorScheme.fromSeed(seedColor: const Color(0xFFFFFFFF)), const Color(0xFF006874)); + + // Variant.fidelity. + await _testFilledButtonColor( + tester, + ColorScheme.fromSeed( + seedColor: const Color(0xFF000000), + dynamicSchemeVariant: DynamicSchemeVariant.fidelity + ), + const Color(0xFF000000) + ); + await _testFilledButtonColor( + tester, + ColorScheme.fromSeed( + seedColor: const Color(0xFF00FF00), + dynamicSchemeVariant: DynamicSchemeVariant.fidelity + ), + const Color(0xFF026E00) + ); + await _testFilledButtonColor( + tester, + ColorScheme.fromSeed( + seedColor: const Color(0xFF6559F5), + dynamicSchemeVariant: DynamicSchemeVariant.fidelity + ), + const Color(0xFF3F2CD0) + ); + await _testFilledButtonColor( + tester, + ColorScheme.fromSeed( + seedColor: const Color(0xFFFFFFFF), + dynamicSchemeVariant: DynamicSchemeVariant.fidelity + ), + const Color(0xFF5D5F5F) + ); + }); +} + +Future _testFilledButtonColor(WidgetTester tester, ColorScheme scheme, Color expectation) async { + final GlobalKey key = GlobalKey(); + await tester.pumpWidget(Container()); // reset + await tester.pumpWidget( + MaterialApp( + theme: ThemeData( + colorScheme: scheme, + ), + home: FilledButton( + key: key, + onPressed: () {}, + child: const SizedBox.square(dimension: 200), + ), + ), + ); + + + final Finder buttonMaterial = find.descendant( + of: find.byType(FilledButton), + matching: find.byType(Material), + ); + final Material material = tester.widget(buttonMaterial); + + expect(material.color, expectation); } diff --git a/packages/flutter/test/widgets/default_text_editing_shortcuts_test.dart b/packages/flutter/test/widgets/default_text_editing_shortcuts_test.dart index 96b4c9c5353766a430377b2745b614eca7e4bb53..3c5b2b971eb9b474dbca9e483eafdf685e542a69 100644 --- a/packages/flutter/test/widgets/default_text_editing_shortcuts_test.dart +++ b/packages/flutter/test/widgets/default_text_editing_shortcuts_test.dart @@ -487,8 +487,8 @@ void main() { }, variant: macOSOnly); }, skip: kIsWeb); // [intended] specific tests target non-web. - group('Linux does accept numpad shortcuts', () { - testWidgets('when numlock is locked', (WidgetTester tester) async { + group('Linux numpad shortcuts', () { + testWidgets('are triggered when numlock is locked', (WidgetTester tester) async { final FocusNode editable = FocusNode(); addTearDown(editable.dispose); final FocusNode spy = FocusNode(); @@ -577,7 +577,7 @@ void main() { expect((state.lastIntent! as DeleteToNextWordBoundaryIntent).forward, true); }, variant: TargetPlatformVariant.only(TargetPlatform.linux)); - testWidgets('when numlock is unlocked', (WidgetTester tester) async { + testWidgets('are triggered when numlock is unlocked', (WidgetTester tester) async { final FocusNode editable = FocusNode(); addTearDown(editable.dispose); final FocusNode spy = FocusNode(); @@ -664,7 +664,79 @@ void main() { expect(state.lastIntent, isA()); expect((state.lastIntent! as DeleteToNextWordBoundaryIntent).forward, true); }, variant: TargetPlatformVariant.only(TargetPlatform.linux)); - }, skip: kIsWeb); // [intended] specific tests target non-web. + + testWidgets('update the editable text content when triggered on non-web', (WidgetTester tester) async { + final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); + final TextEditingController controller = TextEditingController(text: 'Flutter'); + addTearDown(controller.dispose); + + await tester.pumpWidget(MaterialApp( + home: Align( + alignment: Alignment.topLeft, + child: EditableText( + controller: controller, + autofocus: true, + focusNode: focusNode, + style: const TextStyle(fontSize: 10.0), + cursorColor: Colors.blue, + backgroundCursorColor: Colors.grey, + ), + ), + )); + + // Verify that NumLock is unlocked. + expect(HardwareKeyboard.instance.lockModesEnabled.contains(KeyboardLockMode.numLock), isFalse); + + await tester.enterText(find.byType(EditableText), 'Flutter'); + expect(controller.selection.end, 7); + + await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.numpad4)); + // Verify the cursor moved to the left (numpad4). + expect(controller.selection.end, 6); + }, + variant: TargetPlatformVariant.only(TargetPlatform.linux), + skip: kIsWeb, // [intended] Non-web test. + ); + + testWidgets('do not update the editable text content when triggered on web', (WidgetTester tester) async { + final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); + final TextEditingController controller = TextEditingController(text: 'Flutter'); + addTearDown(controller.dispose); + + await tester.pumpWidget(MaterialApp( + home: Align( + alignment: Alignment.topLeft, + child: EditableText( + controller: controller, + autofocus: true, + focusNode: focusNode, + style: const TextStyle(fontSize: 10.0), + cursorColor: Colors.blue, + backgroundCursorColor: Colors.grey, + ), + ), + )); + + // Verify that NumLock is unlocked. + expect(HardwareKeyboard.instance.lockModesEnabled.contains(KeyboardLockMode.numLock), isFalse); + + await tester.enterText(find.byType(EditableText), 'Flutter'); + expect(controller.selection.end, 7); + + await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.numpad4)); + // On web, the editable text would have been updated by the browser. + // In the flutter test environment, the browser logic is not called + // so the editable content is not updated when a shortcut is triggered. + // This is the intended result, this test is checking that numpad shortcuts + // have no effect on the web (their intent is set to DoNothingAndStopPropagationTextIntent). + expect(controller.selection.end, 7); + }, + variant: TargetPlatformVariant.only(TargetPlatform.linux), + skip: !kIsWeb, // [intended] Web only. + ); + }); } class ActionSpy extends StatefulWidget { diff --git a/packages/flutter/test/widgets/editable_text_test.dart b/packages/flutter/test/widgets/editable_text_test.dart index 63a9f40491716e7ae6572dfbd1dd70b0badf94a8..f752d192392d3cdb9f37531dc34b2629d3e5e50b 100644 --- a/packages/flutter/test/widgets/editable_text_test.dart +++ b/packages/flutter/test/widgets/editable_text_test.dart @@ -17264,6 +17264,27 @@ void main() { await tester.pumpAndSettle(); expect(scrollController.offset, 75.0); }); + + testWidgets('Can implement TextEditingController', (WidgetTester tester) async { + await tester.pumpWidget( + MediaQuery( + data: const MediaQueryData(), + child: Directionality( + textDirection: TextDirection.ltr, + child: EditableText( + autofocus: true, + backgroundCursorColor: Colors.grey, + controller: _TextEditingControllerImpl(), + focusNode: focusNode, + style: textStyle, + cursorColor: cursorColor, + ), + ), + ), + ); + + expect(tester.takeException(), isNull); + }); } class UnsettableController extends TextEditingController { @@ -17626,6 +17647,36 @@ class _AccentColorTextEditingController extends TextEditingController { } } +class _TextEditingControllerImpl extends ChangeNotifier implements TextEditingController { + final TextEditingController _innerController = TextEditingController(); + + @override + void clear() => _innerController.clear(); + + @override + void clearComposing() => _innerController.clearComposing(); + + @override + TextSelection get selection => _innerController.selection; + @override + set selection(TextSelection newSelection) => _innerController.selection = newSelection; + + @override + String get text => _innerController.text; + @override + set text(String newText) => _innerController.text = newText; + + @override + TextSpan buildTextSpan({required BuildContext context, TextStyle? style, required bool withComposing}) { + return _innerController.buildTextSpan(context: context, style: style, withComposing: withComposing); + } + + @override + TextEditingValue get value => _innerController.value; + @override + set value(TextEditingValue newValue) => _innerController.value = newValue; +} + class _TestScrollController extends ScrollController { bool get attached => hasListeners; } diff --git a/packages/flutter/test/widgets/routes_test.dart b/packages/flutter/test/widgets/routes_test.dart index 49508884e74daf813ec8db3d865ef5d8ced9dd3b..3c8cd459b1d9d8885a56b9d9b7a7fe4ba6a2e2d0 100644 --- a/packages/flutter/test/widgets/routes_test.dart +++ b/packages/flutter/test/widgets/routes_test.dart @@ -1736,7 +1736,7 @@ void main() { semantics.dispose(); }, variant: const TargetPlatformVariant({TargetPlatform.iOS})); - testWidgets('focus traverse correct when pop multiple page simultaneously', (WidgetTester tester) async { + testWidgets('focus traversal is correct when popping multiple pages simultaneously', (WidgetTester tester) async { // Regression test: https://github.com/flutter/flutter/issues/48903 final GlobalKey navigatorKey = GlobalKey(); await tester.pumpWidget(MaterialApp( diff --git a/packages/flutter/test/widgets/two_dimensional_utils.dart b/packages/flutter/test/widgets/two_dimensional_utils.dart index dd13a277c396e60c28a2c60a87bd33832b609d81..1321786fbd36b80b369fd6a209a09ff2f568f7c3 100644 --- a/packages/flutter/test/widgets/two_dimensional_utils.dart +++ b/packages/flutter/test/widgets/two_dimensional_utils.dart @@ -510,3 +510,39 @@ class TestParentDataWidget extends ParentDataWidget { @override Type get debugTypicalAncestorWidgetClass => SimpleBuilderTableViewport; } + +class KeepAliveOnlyWhenHovered extends StatefulWidget { + const KeepAliveOnlyWhenHovered({ required this.child, super.key }); + + final Widget child; + + @override + KeepAliveOnlyWhenHoveredState createState() => KeepAliveOnlyWhenHoveredState(); +} + +class KeepAliveOnlyWhenHoveredState extends State with AutomaticKeepAliveClientMixin { + bool _hovered = false; + + @override + bool get wantKeepAlive => _hovered; + + @override + Widget build(BuildContext context) { + super.build(context); + return MouseRegion( + onEnter: (_) { + setState(() { + _hovered = true; + updateKeepAlive(); + }); + }, + onExit: (_) { + setState(() { + _hovered = false; + updateKeepAlive(); + }); + }, + child: widget.child, + ); + } +} diff --git a/packages/flutter/test/widgets/two_dimensional_viewport_test.dart b/packages/flutter/test/widgets/two_dimensional_viewport_test.dart index a44bf49ce99dcdb610c628ec30d192d94b787279..a85f384c76af16a0b73daf459b7f7d5e8b1bfbee 100644 --- a/packages/flutter/test/widgets/two_dimensional_viewport_test.dart +++ b/packages/flutter/test/widgets/two_dimensional_viewport_test.dart @@ -735,6 +735,66 @@ void main() { ); }); + testWidgets('Ensure KeepAlive widget is not held onto when it no longer should be kept alive offscreen', (WidgetTester tester) async { + // Regression test for https://github.com/flutter/flutter/issues/138977 + final UniqueKey checkBoxKey = UniqueKey(); + final Widget originCell = KeepAliveOnlyWhenHovered( + key: checkBoxKey, + child: const SizedBox.square(dimension: 200), + ); + const Widget otherCell = SizedBox.square(dimension: 200, child: Placeholder()); + final ScrollController verticalController = ScrollController(); + addTearDown(verticalController.dispose); + final TwoDimensionalChildListDelegate listDelegate = TwoDimensionalChildListDelegate( + children: >[ + [originCell, otherCell, otherCell, otherCell, otherCell], + [otherCell, otherCell, otherCell, otherCell, otherCell], + [otherCell, otherCell, otherCell, otherCell, otherCell], + [otherCell, otherCell, otherCell, otherCell, otherCell], + [otherCell, otherCell, otherCell, otherCell, otherCell], + ], + ); + addTearDown(listDelegate.dispose); + + await tester.pumpWidget(simpleListTest( + delegate: listDelegate, + verticalDetails: ScrollableDetails.vertical(controller: verticalController), + )); + await tester.pumpAndSettle(); + expect(find.byKey(checkBoxKey), findsOneWidget); + + // Scroll away, should not be kept alive (disposed). + verticalController.jumpTo(verticalController.position.maxScrollExtent); + await tester.pump(); + expect(find.byKey(checkBoxKey), findsNothing); + + // Bring back into view + verticalController.jumpTo(0.0); + await tester.pump(); + expect(find.byKey(checkBoxKey), findsOneWidget); + + // Hover over widget to make it keep alive. + final TestGesture gesture = await tester.createGesture( + kind: PointerDeviceKind.mouse, + ); + await gesture.addPointer(location: Offset.zero); + addTearDown(gesture.removePointer); + await tester.pump(); + await gesture.moveTo(tester.getCenter(find.byKey(checkBoxKey))); + await tester.pump(); + + // Scroll away, should be kept alive still. + verticalController.jumpTo(verticalController.position.maxScrollExtent); + await tester.pump(); + expect(find.byKey(checkBoxKey), findsOneWidget); + + // Move the pointer outside the widget bounds to trigger exit event + // and remove it from keep alive bucket. + await gesture.moveTo(const Offset(300, 300)); + await tester.pump(); + expect(find.byKey(checkBoxKey), findsNothing); + }); + testWidgets('list delegate will not add automatic keep alives', (WidgetTester tester) async { final UniqueKey checkBoxKey = UniqueKey(); final Widget originCell = SizedBox.square( diff --git a/packages/flutter/test/widgets/widget_inspector_test.dart b/packages/flutter/test/widgets/widget_inspector_test.dart index b40f8ed905e5a3b7f847c19ba3cc4ef3991ba9a1..32808e30343a1c29e43c6177e144a91ae70b0d9a 100644 --- a/packages/flutter/test/widgets/widget_inspector_test.dart +++ b/packages/flutter/test/widgets/widget_inspector_test.dart @@ -3765,6 +3765,49 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService { skip: !WidgetInspectorService.instance.isWidgetCreationTracked(), // [intended] Test requires --track-widget-creation flag. ); + testWidgets('ext.flutter.inspector.widgetLocationIdMap', + (WidgetTester tester) async { + service.rebuildCount = 0; + + await tester.pumpWidget(const ClockDemo()); + + final Element clockDemoElement = find.byType(ClockDemo).evaluate().first; + + service.setSelection(clockDemoElement, 'my-group'); + final Map jsonObject = (await service.testExtension( + WidgetInspectorServiceExtensions.getSelectedWidget.name, + {'objectGroup': 'my-group'}, + ))! as Map; + final Map creationLocation = + jsonObject['creationLocation']! as Map; + final String file = creationLocation['file']! as String; + expect(file, endsWith('widget_inspector_test.dart')); + + final Map locationMapJson = (await service.testExtension( + WidgetInspectorServiceExtensions.widgetLocationIdMap.name, + {}, + ))! as Map; + + final Map widgetTestLocations = + locationMapJson[file]! as Map; + expect(widgetTestLocations, isNotNull); + + final List ids = widgetTestLocations['ids']! as List; + expect(ids.length, greaterThan(0)); + final List lines = + widgetTestLocations['lines']! as List; + expect(lines.length, equals(ids.length)); + final List columns = + widgetTestLocations['columns']! as List; + expect(columns.length, equals(ids.length)); + final List names = + widgetTestLocations['names']! as List; + expect(names.length, equals(ids.length)); + expect(names, contains('ClockDemo')); + expect(names, contains('Directionality')); + expect(names, contains('ClockText')); + }, skip: !WidgetInspectorService.instance.isWidgetCreationTracked()); // [intended] Test requires --track-widget-creation flag. + testWidgets('ext.flutter.inspector.trackRebuildDirtyWidgets', (WidgetTester tester) async { service.rebuildCount = 0; @@ -3951,6 +3994,7 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService { expect(rebuildEvents.length, equals(1)); event = removeLastEvent(rebuildEvents); expect(event['startTime'], isA()); + expect(event['frameNumber'], isA()); data = event['events']! as List; newLocations = event['newLocations']! as Map>; fileLocationsMap = event['locations']! as Map>>; @@ -4080,6 +4124,7 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService { expect(repaintEvents.length, equals(1)); event = removeLastEvent(repaintEvents); expect(event['startTime'], isA()); + expect(event['frameNumber'], isA()); data = event['events']! as List; // No new locations were rebuilt. expect(event, isNot(contains('newLocations'))); diff --git a/packages/flutter_tools/lib/src/android/gradle_errors.dart b/packages/flutter_tools/lib/src/android/gradle_errors.dart index 4bc3efaae3c44d760ea0280bb9a28ad5e21316fd..97e1ce89fdeee1cd2c41cd24bea5702c6844645e 100644 --- a/packages/flutter_tools/lib/src/android/gradle_errors.dart +++ b/packages/flutter_tools/lib/src/android/gradle_errors.dart @@ -441,10 +441,17 @@ final GradleHandledError incompatibleKotlinVersionHandler = GradleHandledError( final File gradleFile = project.directory .childDirectory('android') .childFile('build.gradle'); + final File settingsFile = project.directory + .childDirectory('android') + .childFile('settings.gradle'); globals.printBox( '${globals.logger.terminal.warningMark} Your project requires a newer version of the Kotlin Gradle plugin.\n' - 'Find the latest version on https://kotlinlang.org/docs/releases.html#release-details, then update ${gradleFile.path}:\n' - "ext.kotlin_version = ''", + 'Find the latest version on https://kotlinlang.org/docs/releases.html#release-details, then update the \n' + 'version number of the plugin with id "org.jetbrains.kotlin.android" in the plugins block of \n' + '${settingsFile.path}.\n\n' + 'Alternatively (if your project was created before Flutter 3.19), update \n' + '${gradleFile.path}\n' + "ext.kotlin_version = ''", title: _boxTitle, ); return GradleBuildStatus.exit; diff --git a/packages/flutter_tools/lib/src/base/error_handling_io.dart b/packages/flutter_tools/lib/src/base/error_handling_io.dart index 468bfbf70573254fe8a529fc14e4ef8d3d42abd7..072bd5da4a0a76d204576fd32880463c976842ba 100644 --- a/packages/flutter_tools/lib/src/base/error_handling_io.dart +++ b/packages/flutter_tools/lib/src/base/error_handling_io.dart @@ -21,9 +21,13 @@ import 'platform.dart'; // ToolExit and a message that is more clear than the FileSystemException by // itself. -/// On windows this is error code 2: ERROR_FILE_NOT_FOUND, and on +/// On Windows this is error code 2: ERROR_FILE_NOT_FOUND, and on /// macOS/Linux it is error code 2/ENOENT: No such file or directory. -const int kSystemCannotFindFile = 2; +const int kSystemCodeCannotFindFile = 2; + +/// On Windows this error is 3: ERROR_PATH_NOT_FOUND, and on +/// macOS/Linux, it is error code 3/ESRCH: No such process. +const int kSystemCodePathNotFound = 3; /// A [FileSystem] that throws a [ToolExit] on certain errors. /// @@ -72,22 +76,26 @@ class ErrorHandlingFileSystem extends ForwardingFileSystem { /// This method should be preferred to checking if it exists and /// then deleting, because it handles the edge case where the file or directory /// is deleted by a different program between the two calls. - static bool deleteIfExists(FileSystemEntity file, {bool recursive = false}) { - if (!file.existsSync()) { + static bool deleteIfExists(FileSystemEntity entity, {bool recursive = false}) { + if (!entity.existsSync()) { return false; } try { - file.deleteSync(recursive: recursive); + entity.deleteSync(recursive: recursive); } on FileSystemException catch (err) { // Certain error codes indicate the file could not be found. It could have // been deleted by a different program while the tool was running. // if it still exists, the file likely exists on a read-only volume. - if (err.osError?.errorCode != kSystemCannotFindFile || _noExitOnFailure) { + // This check will falsely match "3/ESRCH: No such process" on Linux/macOS, + // but this should be fine since this code should never come up here. + final bool codeCorrespondsToPathOrFileNotFound = err.osError?.errorCode == kSystemCodeCannotFindFile || + err.osError?.errorCode == kSystemCodePathNotFound; + if (!codeCorrespondsToPathOrFileNotFound || _noExitOnFailure) { rethrow; } - if (file.existsSync()) { + if (entity.existsSync()) { throwToolExit( - 'The Flutter tool tried to delete the file or directory ${file.path} but was ' + 'The Flutter tool tried to delete the file or directory ${entity.path} but was ' "unable to. This may be due to the file and/or project's location on a read-only " 'volume. Consider relocating the project and trying again', ); @@ -104,7 +112,7 @@ class ErrorHandlingFileSystem extends ForwardingFileSystem { return _runSync(() => directory(delegate.currentDirectory), platform: _platform); } on FileSystemException catch (err) { // Special handling for OS error 2 for current directory only. - if (err.osError?.errorCode == kSystemCannotFindFile) { + if (err.osError?.errorCode == kSystemCodeCannotFindFile) { throwToolExit( 'Unable to read current working directory. This can happen if the directory the ' 'Flutter tool was run from was moved or deleted.' diff --git a/packages/flutter_tools/lib/src/build_system/targets/ios.dart b/packages/flutter_tools/lib/src/build_system/targets/ios.dart index 9ca6e543a206de25898d5c92929d207bd547a977..56d73f7cfa42cded67692bcbf6e817b363bd047b 100644 --- a/packages/flutter_tools/lib/src/build_system/targets/ios.dart +++ b/packages/flutter_tools/lib/src/build_system/targets/ios.dart @@ -314,6 +314,7 @@ abstract class UnpackIOS extends Target { '--delete', '--filter', '- .DS_Store/', + '--chmod=Du=rwx,Dgo=rx,Fu=rw,Fgo=r', basePath, environment.outputDir.path, ]); diff --git a/packages/flutter_tools/lib/src/build_system/targets/macos.dart b/packages/flutter_tools/lib/src/build_system/targets/macos.dart index 9996664a0a1e53e702c5710d6590906ed12d75bf..e9f48cc88e441e4424d131f5aa8010a8354ecc3f 100644 --- a/packages/flutter_tools/lib/src/build_system/targets/macos.dart +++ b/packages/flutter_tools/lib/src/build_system/targets/macos.dart @@ -60,6 +60,7 @@ abstract class UnpackMacOS extends Target { '--delete', '--filter', '- .DS_Store/', + '--chmod=Du=rwx,Dgo=rx,Fu=rw,Fgo=r', basePath, environment.outputDir.path, ]); diff --git a/packages/flutter_tools/lib/src/commands/test.dart b/packages/flutter_tools/lib/src/commands/test.dart index abaa42f11f3f66848c2ee3ca0f5cd8012edce6e3..9c5519a9cf623f0f89add968daa422ed61d12c20 100644 --- a/packages/flutter_tools/lib/src/commands/test.dart +++ b/packages/flutter_tools/lib/src/commands/test.dart @@ -678,7 +678,7 @@ class TestCommand extends FlutterCommand with DeviceBasedDevelopmentArtifacts { if (build != 0) { throwToolExit('Error: Failed to build asset bundle'); } - if (_needRebuild(assetBundle.entries)) { + if (_needsRebuild(assetBundle.entries, flavor)) { await writeBundle( globals.fs.directory(globals.fs.path.join('build', 'unit_test_assets')), assetBundle.entries, @@ -690,14 +690,25 @@ class TestCommand extends FlutterCommand with DeviceBasedDevelopmentArtifacts { logger: globals.logger, projectDir: globals.fs.currentDirectory, ); + + final File cachedFlavorFile = globals.fs.file( + globals.fs.path.join('build', 'test_cache', 'flavor.txt'), + ); + if (cachedFlavorFile.existsSync()) { + await cachedFlavorFile.delete(); + } + if (flavor != null) { + cachedFlavorFile.createSync(recursive: true); + cachedFlavorFile.writeAsStringSync(flavor); + } } } - bool _needRebuild(Map entries) { + bool _needsRebuild(Map entries, String? flavor) { // TODO(andrewkolos): This logic might fail in the future if we change the - // schema of the contents of the asset manifest file and the user does not - // perform a `flutter clean` after upgrading. - // See https://github.com/flutter/flutter/issues/128563. + // schema of the contents of the asset manifest file and the user does not + // perform a `flutter clean` after upgrading. + // See https://github.com/flutter/flutter/issues/128563. final File manifest = globals.fs.file(globals.fs.path.join('build', 'unit_test_assets', 'AssetManifest.bin')); if (!manifest.existsSync()) { return true; @@ -718,6 +729,17 @@ class TestCommand extends FlutterCommand with DeviceBasedDevelopmentArtifacts { return true; } } + + final File cachedFlavorFile = globals.fs.file( + globals.fs.path.join('build', 'test_cache', 'flavor.txt'), + ); + final String? cachedFlavor = cachedFlavorFile.existsSync() + ? cachedFlavorFile.readAsStringSync() + : null; + if (cachedFlavor != flavor) { + return true; + } + return false; } } diff --git a/packages/flutter_tools/pubspec.yaml b/packages/flutter_tools/pubspec.yaml index 528ae91f69dee4c2a7dd95e753bd7744fbc4a3de..88b03dc399fffb8fcac412aac3cebfecbe144f29 100644 --- a/packages/flutter_tools/pubspec.yaml +++ b/packages/flutter_tools/pubspec.yaml @@ -54,7 +54,7 @@ dependencies: async: 2.11.0 # this dependence use to parse json5 config file json5: ^0.8.0 - unified_analytics: 5.8.8+1 + unified_analytics: 5.8.8+2 cli_config: 0.2.0 graphs: 2.3.1 @@ -122,5 +122,4 @@ dartdoc: # Exclude this package from the hosted API docs. nodoc: true -# TODO: fix checksum -# PUBSPEC CHECKSUM: 067f +# PUBSPEC CHECKSUM: d480 diff --git a/packages/flutter_tools/test/commands.shard/hermetic/test_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/test_test.dart index fa27e70499c0b8abaa96834645e42785ccf7178c..c08115660fe66a02a82dd34142009bc2ba6f814b 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/test_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/test_test.dart @@ -7,6 +7,7 @@ import 'dart:convert'; import 'package:args/command_runner.dart'; import 'package:file/memory.dart'; +import 'package:file_testing/file_testing.dart'; import 'package:flutter_tools/src/base/async_guard.dart'; import 'package:flutter_tools/src/base/common.dart'; import 'package:flutter_tools/src/base/file_system.dart'; @@ -1170,6 +1171,59 @@ dev_dependencies: DeviceManager: () => _FakeDeviceManager([]), }); + testUsingContext('correctly considers --flavor when validating the cached asset bundle', () async { + final FakeFlutterTestRunner testRunner = FakeFlutterTestRunner(0); + fs.file('vanilla.txt').writeAsStringSync('vanilla'); + fs.file('flavorless.txt').writeAsStringSync('flavorless'); + fs.file('pubspec.yaml').writeAsStringSync(''' +flutter: + assets: + - path: vanilla.txt + flavors: + - vanilla + - flavorless.txt +dev_dependencies: + flutter_test: + sdk: flutter + integration_test: + sdk: flutter'''); + final TestCommand testCommand = TestCommand(testRunner: testRunner); + final CommandRunner commandRunner = createTestCommandRunner(testCommand); + + const List buildArgsFlavorless = [ + 'test', + '--no-pub', + ]; + + const List buildArgsVanilla = [ + 'test', + '--no-pub', + '--flavor', + 'vanilla', + ]; + + final File builtVanillaAssetFile = fs.file( + fs.path.join('build', 'unit_test_assets', 'vanilla.txt'), + ); + final File builtFlavorlessAssetFile = fs.file( + fs.path.join('build', 'unit_test_assets', 'flavorless.txt'), + ); + + await commandRunner.run(buildArgsVanilla); + await commandRunner.run(buildArgsFlavorless); + + expect(builtVanillaAssetFile, isNot(exists)); + expect(builtFlavorlessAssetFile, exists); + + await commandRunner.run(buildArgsVanilla); + + expect(builtVanillaAssetFile, exists); + }, overrides: { + FileSystem: () => fs, + ProcessManager: () => FakeProcessManager.empty(), + DeviceManager: () => _FakeDeviceManager([]), + }); + testUsingContext("Don't build the asset manifest if --no-test-assets if informed", () async { final FakeFlutterTestRunner testRunner = FakeFlutterTestRunner(0); diff --git a/packages/flutter_tools/test/general.shard/android/gradle_errors_test.dart b/packages/flutter_tools/test/general.shard/android/gradle_errors_test.dart index 066915966029f4fec348f3eba26eaa5e66957b48..ba1fe95b4a92c88873d72d6ebd0c99900e748092 100644 --- a/packages/flutter_tools/test/general.shard/android/gradle_errors_test.dart +++ b/packages/flutter_tools/test/general.shard/android/gradle_errors_test.dart @@ -814,13 +814,18 @@ Execution failed for task ':app:generateDebugFeatureTransitiveDeps'. expect( testLogger.statusText, contains( - '\n' - '┌─ Flutter Fix ──────────────────────────────────────────────────────────────────────────────┐\n' - '│ [!] Your project requires a newer version of the Kotlin Gradle plugin. │\n' - '│ Find the latest version on https://kotlinlang.org/docs/releases.html#release-details, then │\n' - '│ update /android/build.gradle: │\n' - "│ ext.kotlin_version = '' │\n" - '└────────────────────────────────────────────────────────────────────────────────────────────┘\n' + '\n' + '┌─ Flutter Fix ────────────────────────────────────────────────────────────────────────────────┐\n' + '│ [!] Your project requires a newer version of the Kotlin Gradle plugin. │\n' + '│ Find the latest version on https://kotlinlang.org/docs/releases.html#release-details, then │\n' + '│ update the │\n' + '│ version number of the plugin with id "org.jetbrains.kotlin.android" in the plugins block of │\n' + '│ /android/settings.gradle. │\n' + '│ │\n' + '│ Alternatively (if your project was created before Flutter 3.19), update │\n' + '│ /android/build.gradle │\n' + "│ ext.kotlin_version = '' │\n" + '└──────────────────────────────────────────────────────────────────────────────────────────────┘\n' ) ); }, overrides: { diff --git a/packages/flutter_tools/test/general.shard/base/error_handling_io_test.dart b/packages/flutter_tools/test/general.shard/base/error_handling_io_test.dart index ef2752b60477dc375efcb482b68e8a502f1c922f..f6a56a58d6ad9d004aa68007a8d81ea26cbd30d9 100644 --- a/packages/flutter_tools/test/general.shard/base/error_handling_io_test.dart +++ b/packages/flutter_tools/test/general.shard/base/error_handling_io_test.dart @@ -98,6 +98,24 @@ void main() { }, throwsFileSystemException()); }); + testWithoutContext('deleteIfExists throws tool exit if the path is not found on Windows', () { + final FileExceptionHandler exceptionHandler = FileExceptionHandler(); + final ErrorHandlingFileSystem fileSystem = ErrorHandlingFileSystem( + delegate: MemoryFileSystem.test(opHandle: exceptionHandler.opHandle), + platform: windowsPlatform, + ); + final File file = fileSystem.file(fileSystem.path.join('directory', 'file')) + ..createSync(recursive: true); + + exceptionHandler.addError( + file, + FileSystemOp.delete, + FileSystemException('', file.path, const OSError('', 2)), + ); + + expect(() => ErrorHandlingFileSystem.deleteIfExists(file), throwsToolExit()); + }); + group('throws ToolExit on Windows', () { const int kDeviceFull = 112; const int kUserMappedSectionOpened = 1224; @@ -571,14 +589,14 @@ void main() { testWithoutContext('When the current working directory disappears', () async { final ErrorHandlingFileSystem fileSystem = ErrorHandlingFileSystem( - delegate: ThrowsOnCurrentDirectoryFileSystem(kSystemCannotFindFile), + delegate: ThrowsOnCurrentDirectoryFileSystem(kSystemCodeCannotFindFile), platform: linuxPlatform, ); expect(() => fileSystem.currentDirectory, throwsToolExit(message: 'Unable to read current working directory')); }); - testWithoutContext('Rethrows os error $kSystemCannotFindFile', () { + testWithoutContext('Rethrows os error $kSystemCodeCannotFindFile', () { final ErrorHandlingFileSystem fileSystem = ErrorHandlingFileSystem( delegate: MemoryFileSystem.test(opHandle: exceptionHandler.opHandle), platform: linuxPlatform, @@ -588,11 +606,11 @@ void main() { exceptionHandler.addError( file, FileSystemOp.read, - FileSystemException('', file.path, const OSError('', kSystemCannotFindFile)), + FileSystemException('', file.path, const OSError('', kSystemCodeCannotFindFile)), ); // Error is not caught by other operations. - expect(() => fileSystem.file('foo').readAsStringSync(), throwsFileSystemException(kSystemCannotFindFile)); + expect(() => fileSystem.file('foo').readAsStringSync(), throwsFileSystemException(kSystemCodeCannotFindFile)); }); }); diff --git a/packages/flutter_tools/test/general.shard/build_system/targets/ios_test.dart b/packages/flutter_tools/test/general.shard/build_system/targets/ios_test.dart index 0d37f90460a1ef87846145ab236e4b0ea3ca1676..e98a71eacf6538efc7cf979ab12e5b23b93bc38e 100644 --- a/packages/flutter_tools/test/general.shard/build_system/targets/ios_test.dart +++ b/packages/flutter_tools/test/general.shard/build_system/targets/ios_test.dart @@ -542,6 +542,7 @@ void main() { '--delete', '--filter', '- .DS_Store/', + '--chmod=Du=rwx,Dgo=rx,Fu=rw,Fgo=r', 'Artifact.flutterFramework.TargetPlatform.ios.debug.EnvironmentType.physical', outputDir.path, ]); @@ -598,6 +599,7 @@ void main() { '--delete', '--filter', '- .DS_Store/', + '--chmod=Du=rwx,Dgo=rx,Fu=rw,Fgo=r', 'Artifact.flutterFramework.TargetPlatform.ios.debug.EnvironmentType.simulator', outputDir.path, ], diff --git a/packages/flutter_tools/test/general.shard/build_system/targets/macos_test.dart b/packages/flutter_tools/test/general.shard/build_system/targets/macos_test.dart index 680d826b4967fb42c58bfe5fd1f8c03b4da0e138..21898bd3dcf88b23c9104d877fcb19daa888f809 100644 --- a/packages/flutter_tools/test/general.shard/build_system/targets/macos_test.dart +++ b/packages/flutter_tools/test/general.shard/build_system/targets/macos_test.dart @@ -73,6 +73,7 @@ void main() { '--delete', '--filter', '- .DS_Store/', + '--chmod=Du=rwx,Dgo=rx,Fu=rw,Fgo=r', 'Artifact.flutterMacOSFramework.debug', environment.outputDir.path, ], @@ -133,6 +134,7 @@ void main() { '--delete', '--filter', '- .DS_Store/', + '--chmod=Du=rwx,Dgo=rx,Fu=rw,Fgo=r', // source 'Artifact.flutterMacOSFramework.debug', // destination diff --git a/packages/flutter_tools/test/host_cross_arch.shard/ios_content_validation_test.dart b/packages/flutter_tools/test/host_cross_arch.shard/ios_content_validation_test.dart index 2e0587524e68f5be895428ac48d2e2b4af84b795..0f7befcac57fd424ebb756dfaeec6d38f40710ae 100644 --- a/packages/flutter_tools/test/host_cross_arch.shard/ios_content_validation_test.dart +++ b/packages/flutter_tools/test/host_cross_arch.shard/ios_content_validation_test.dart @@ -422,10 +422,16 @@ void main() { .whereType() .singleWhere((Directory directory) => fileSystem.path.extension(directory.path) == '.app'); + final Directory flutterFrameworkDir = fileSystem.directory( + fileSystem.path.join( + appBundle.path, + 'Frameworks', + 'Flutter.framework', + ), + ); + final String flutterFramework = fileSystem.path.join( - appBundle.path, - 'Frameworks', - 'Flutter.framework', + flutterFrameworkDir.path, 'Flutter', ); @@ -456,6 +462,11 @@ void main() { ], ); expect(appCodesign, const ProcessResultMatcher()); + + // Check read/write permissions are being correctly set + final String rawStatString = flutterFrameworkDir.statSync().modeString(); + final String statString = rawStatString.substring(rawStatString.length - 9); + expect(statString, 'rwxr-xr-x'); }); }, skip: !platform.isMacOS, // [intended] only makes sense for macos platform. timeout: const Timeout(Duration(minutes: 10)) diff --git a/packages/flutter_tools/test/host_cross_arch.shard/macos_content_validation_test.dart b/packages/flutter_tools/test/host_cross_arch.shard/macos_content_validation_test.dart index 5568ad2f9a0b801c04029f97b60f375c52c13df6..2ff219c76419c2fefd2cc732c44ec7cfc0b0517b 100644 --- a/packages/flutter_tools/test/host_cross_arch.shard/macos_content_validation_test.dart +++ b/packages/flutter_tools/test/host_cross_arch.shard/macos_content_validation_test.dart @@ -164,6 +164,11 @@ void main() { ), ); + // Check read/write permissions are being correctly set + final String rawStatString = outputFlutterFramework.statSync().modeString(); + final String statString = rawStatString.substring(rawStatString.length - 9); + expect(statString, 'rwxr-xr-x'); + // Check complicated macOS framework symlink structure. final Link current = outputFlutterFramework.childDirectory('Versions').childLink('Current');