From 2e01eef5aa70b1d6db7f019dbf9af0ebaaadb68f Mon Sep 17 00:00:00 2001 From: Hans Muller Date: Wed, 21 Aug 2019 14:02:19 -0700 Subject: [PATCH] Added InheritedTheme (#38583) --- .../lib/src/material/banner_theme.dart | 8 +- .../lib/src/material/button_theme.dart | 8 +- .../flutter/lib/src/material/chip_theme.dart | 8 +- .../lib/src/material/divider_theme.dart | 8 +- .../flutter/lib/src/material/list_tile.dart | 16 +- .../flutter/lib/src/material/popup_menu.dart | 31 +- .../lib/src/material/popup_menu_theme.dart | 8 +- .../lib/src/material/slider_theme.dart | 9 +- packages/flutter/lib/src/material/theme.dart | 8 +- .../src/material/toggle_buttons_theme.dart | 8 +- .../lib/src/material/tooltip_theme.dart | 8 +- .../flutter/lib/src/widgets/icon_theme.dart | 9 +- .../lib/src/widgets/inherited_theme.dart | 146 ++++ packages/flutter/lib/src/widgets/text.dart | 17 +- packages/flutter/lib/widgets.dart | 1 + .../test/material/inherited_theme_test.dart | 723 ++++++++++++++++++ .../test/widgets/inherited_theme_test.dart | 149 ++++ 17 files changed, 1147 insertions(+), 18 deletions(-) create mode 100644 packages/flutter/lib/src/widgets/inherited_theme.dart create mode 100644 packages/flutter/test/material/inherited_theme_test.dart create mode 100644 packages/flutter/test/widgets/inherited_theme_test.dart diff --git a/packages/flutter/lib/src/material/banner_theme.dart b/packages/flutter/lib/src/material/banner_theme.dart index 252389c5b6..83cc28d4c0 100644 --- a/packages/flutter/lib/src/material/banner_theme.dart +++ b/packages/flutter/lib/src/material/banner_theme.dart @@ -116,7 +116,7 @@ class MaterialBannerThemeData extends Diagnosticable { /// /// Values specified here are used for [MaterialBanner] properties that are not /// given an explicit non-null value. -class MaterialBannerTheme extends InheritedWidget { +class MaterialBannerTheme extends InheritedTheme { /// Creates a banner theme that controls the configurations for /// [MaterialBanner]s in its widget subtree. const MaterialBannerTheme({ @@ -144,6 +144,12 @@ class MaterialBannerTheme extends InheritedWidget { return popupMenuTheme?.data ?? Theme.of(context).bannerTheme; } + @override + Widget wrap(BuildContext context, Widget child) { + final MaterialBannerTheme ancestorTheme = context.ancestorWidgetOfExactType(MaterialBannerTheme); + return identical(this, ancestorTheme) ? child : MaterialBannerTheme(data: data, child: child); + } + @override bool updateShouldNotify(MaterialBannerTheme oldWidget) => data != oldWidget.data; } diff --git a/packages/flutter/lib/src/material/button_theme.dart b/packages/flutter/lib/src/material/button_theme.dart index 67afdc4ab6..c841b0e214 100644 --- a/packages/flutter/lib/src/material/button_theme.dart +++ b/packages/flutter/lib/src/material/button_theme.dart @@ -63,7 +63,7 @@ enum ButtonBarLayoutBehavior { /// based on the ambient button theme. /// * [RawMaterialButton], which can be used to configure a button that doesn't /// depend on any inherited themes. -class ButtonTheme extends InheritedWidget { +class ButtonTheme extends InheritedTheme { /// Creates a button theme. /// /// The [textTheme], [minWidth], [height], and [colorScheme] arguments @@ -230,6 +230,12 @@ class ButtonTheme extends InheritedWidget { return buttonTheme; } + @override + Widget wrap(BuildContext context, Widget child) { + final ButtonTheme ancestorTheme = context.ancestorWidgetOfExactType(ButtonTheme); + return identical(this, ancestorTheme) ? child : ButtonTheme.fromButtonThemeData(data: data, child: child); + } + @override bool updateShouldNotify(ButtonTheme oldWidget) => data != oldWidget.data; } diff --git a/packages/flutter/lib/src/material/chip_theme.dart b/packages/flutter/lib/src/material/chip_theme.dart index 1cf697c84b..7b3968147e 100644 --- a/packages/flutter/lib/src/material/chip_theme.dart +++ b/packages/flutter/lib/src/material/chip_theme.dart @@ -40,7 +40,7 @@ import 'theme_data.dart'; /// theme. /// * [ThemeData], which describes the overall theme information for the /// application. -class ChipTheme extends InheritedWidget { +class ChipTheme extends InheritedTheme { /// Applies the given theme [data] to [child]. /// /// The [data] and [child] arguments must not be null. @@ -89,6 +89,12 @@ class ChipTheme extends InheritedWidget { return inheritedTheme?.data ?? Theme.of(context).chipTheme; } + @override + Widget wrap(BuildContext context, Widget child) { + final ChipTheme ancestorTheme = context.ancestorWidgetOfExactType(ChipTheme); + return identical(this, ancestorTheme) ? child : ChipTheme(data: data, child: child); + } + @override bool updateShouldNotify(ChipTheme oldWidget) => data != oldWidget.data; } diff --git a/packages/flutter/lib/src/material/divider_theme.dart b/packages/flutter/lib/src/material/divider_theme.dart index e1aae3aadb..2c97f6db8c 100644 --- a/packages/flutter/lib/src/material/divider_theme.dart +++ b/packages/flutter/lib/src/material/divider_theme.dart @@ -132,7 +132,7 @@ class DividerThemeData extends Diagnosticable { /// An inherited widget that defines the configuration for /// [Divider]s, [VerticalDividers]s, dividers between [ListTile]s, and dividers /// between rows in [DataTable]s in this widget's subtree. -class DividerTheme extends InheritedWidget { +class DividerTheme extends InheritedTheme { /// Creates a divider theme that controls the configurations for /// [Divider]s, [VerticalDividers]s, dividers between [ListTile]s, and dividers /// between rows in [DataTable]s in its widget subtree. @@ -163,6 +163,12 @@ class DividerTheme extends InheritedWidget { return dividerTheme?.data ?? Theme.of(context).dividerTheme; } + @override + Widget wrap(BuildContext context, Widget child) { + final DividerTheme ancestorTheme = context.ancestorWidgetOfExactType(DividerTheme); + return identical(this, ancestorTheme) ? child : DividerTheme(data: data, child: child); + } + @override bool updateShouldNotify(DividerTheme oldWidget) => data != oldWidget.data; } diff --git a/packages/flutter/lib/src/material/list_tile.dart b/packages/flutter/lib/src/material/list_tile.dart index fb7fc4b004..4f45c932de 100644 --- a/packages/flutter/lib/src/material/list_tile.dart +++ b/packages/flutter/lib/src/material/list_tile.dart @@ -36,7 +36,7 @@ enum ListTileStyle { /// /// The [Drawer] widget specifies a tile theme for its children which sets /// [style] to [ListTileStyle.drawer]. -class ListTileTheme extends InheritedWidget { +class ListTileTheme extends InheritedTheme { /// Creates a list tile theme that controls the color and style parameters for /// [ListTile]s. const ListTileTheme({ @@ -115,6 +115,20 @@ class ListTileTheme extends InheritedWidget { return result ?? const ListTileTheme(); } + @override + Widget wrap(BuildContext context, Widget child) { + final ListTileTheme ancestorTheme = context.ancestorWidgetOfExactType(ListTileTheme); + return identical(this, ancestorTheme) ? child : ListTileTheme( + dense: dense, + style: style, + selectedColor: selectedColor, + iconColor: iconColor, + textColor: textColor, + contentPadding: contentPadding, + child: child, + ); + } + @override bool updateShouldNotify(ListTileTheme oldWidget) { return dense != oldWidget.dense diff --git a/packages/flutter/lib/src/material/popup_menu.dart b/packages/flutter/lib/src/material/popup_menu.dart index d9053fc84f..7d31c6dc7c 100644 --- a/packages/flutter/lib/src/material/popup_menu.dart +++ b/packages/flutter/lib/src/material/popup_menu.dart @@ -264,7 +264,7 @@ class PopupMenuItemState> extends State { duration: kThemeChangeDuration, child: Baseline( baseline: widget.height - _kBaselineOffsetFromBottom, - baselineType: style.textBaseline, + baselineType: style.textBaseline ?? TextBaseline.alphabetic, child: buildChild(), ), ); @@ -608,6 +608,8 @@ class _PopupMenuRoute extends PopupRoute { this.semanticLabel, this.shape, this.color, + this.showMenuContext, + this.captureInheritedThemes, }); final RelativeRect position; @@ -619,6 +621,8 @@ class _PopupMenuRoute extends PopupRoute { final ShapeBorder shape; final Color color; final PopupMenuThemeData popupMenuTheme; + final BuildContext showMenuContext; + final bool captureInheritedThemes; @override Animation createAnimation() { @@ -656,10 +660,15 @@ class _PopupMenuRoute extends PopupRoute { } Widget menu = _PopupMenu(route: this, semanticLabel: semanticLabel); - if (popupMenuTheme != null) - menu = PopupMenuTheme(data: PopupMenuThemeData(textStyle: popupMenuTheme.textStyle), child: menu); - if (theme != null) - menu = Theme(data: theme, child: menu); + if (captureInheritedThemes) { + menu = InheritedTheme.captureAll(showMenuContext, menu); + } else { + // For the sake of backwards compatibility. An (unlikely) app that relied + // on having menus only inherit from the material Theme could set + // captureInheritedThemes to false and get the original behvaior. + if (theme != null) + menu = Theme(data: theme, child: menu); + } return MediaQuery.removePadding( context: context, @@ -743,10 +752,12 @@ Future showMenu({ String semanticLabel, ShapeBorder shape, Color color, + bool captureInheritedThemes = true, }) { assert(context != null); assert(position != null); assert(items != null && items.isNotEmpty); + assert(captureInheritedThemes != null); assert(debugCheckHasMaterialLocalizations(context)); String label = semanticLabel; @@ -770,6 +781,8 @@ Future showMenu({ barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel, shape: shape, color: color, + showMenuContext: context, + captureInheritedThemes: captureInheritedThemes, )); } @@ -862,9 +875,11 @@ class PopupMenuButton extends StatefulWidget { this.enabled = true, this.shape, this.color, + this.captureInheritedThemes = true, }) : assert(itemBuilder != null), assert(offset != null), assert(enabled != null), + assert(captureInheritedThemes != null), assert(!(child != null && icon != null)), // fails if passed both parameters super(key: key); @@ -943,6 +958,11 @@ class PopupMenuButton extends StatefulWidget { /// Theme.of(context).cardColor is used. final Color color; + /// If true (the default) then the menu will be wrapped with copies + /// of the [InheritedThemes], like [Theme] and [PopupMenuTheme], which + /// are defined above the [BuildContext] where the menu is shown. + final bool captureInheritedThemes; + @override _PopupMenuButtonState createState() => _PopupMenuButtonState(); } @@ -970,6 +990,7 @@ class _PopupMenuButtonState extends State> { position: position, shape: widget.shape ?? popupMenuTheme.shape, color: widget.color ?? popupMenuTheme.color, + captureInheritedThemes: widget.captureInheritedThemes, ) .then((T newValue) { if (!mounted) diff --git a/packages/flutter/lib/src/material/popup_menu_theme.dart b/packages/flutter/lib/src/material/popup_menu_theme.dart index fa2904479c..bb0738d0f0 100644 --- a/packages/flutter/lib/src/material/popup_menu_theme.dart +++ b/packages/flutter/lib/src/material/popup_menu_theme.dart @@ -121,7 +121,7 @@ class PopupMenuThemeData extends Diagnosticable { /// /// Values specified here are used for popup menu properties that are not /// given an explicit non-null value. -class PopupMenuTheme extends InheritedWidget { +class PopupMenuTheme extends InheritedTheme { /// Creates a popup menu theme that controls the configurations for /// popup menus in its widget subtree. /// @@ -149,6 +149,12 @@ class PopupMenuTheme extends InheritedWidget { return popupMenuTheme?.data ?? Theme.of(context).popupMenuTheme; } + @override + Widget wrap(BuildContext context, Widget child) { + final PopupMenuTheme ancestorTheme = context.ancestorWidgetOfExactType(PopupMenuTheme); + return identical(this, ancestorTheme) ? child : PopupMenuTheme(data: data, child: child); + } + @override bool updateShouldNotify(PopupMenuTheme oldWidget) => data != oldWidget.data; } diff --git a/packages/flutter/lib/src/material/slider_theme.dart b/packages/flutter/lib/src/material/slider_theme.dart index 3b909b609a..219d697cf2 100644 --- a/packages/flutter/lib/src/material/slider_theme.dart +++ b/packages/flutter/lib/src/material/slider_theme.dart @@ -134,7 +134,7 @@ import 'theme_data.dart'; /// {@macro flutter.material.slider.seeAlso.rangeSliderValueIndicatorShape} /// {@macro flutter.material.slider.seeAlso.rangeSliderTrackShape} /// {@macro flutter.material.slider.seeAlso.rangeSliderTickMarkShape} -class SliderTheme extends InheritedWidget { +class SliderTheme extends InheritedTheme { /// Applies the given theme [data] to [child]. /// /// The [data] and [child] arguments must not be null. @@ -189,6 +189,12 @@ class SliderTheme extends InheritedWidget { return inheritedTheme != null ? inheritedTheme.data : Theme.of(context).sliderTheme; } + @override + Widget wrap(BuildContext context, Widget child) { + final SliderTheme ancestorTheme = context.ancestorWidgetOfExactType(SliderTheme); + return identical(this, ancestorTheme) ? child : SliderTheme(data: data, child: child); + } + @override bool updateShouldNotify(SliderTheme oldWidget) => data != oldWidget.data; } @@ -2928,4 +2934,3 @@ class RangeLabels { return '$runtimeType($start, $end)'; } } - diff --git a/packages/flutter/lib/src/material/theme.dart b/packages/flutter/lib/src/material/theme.dart index 0cea75d669..1e3a559650 100644 --- a/packages/flutter/lib/src/material/theme.dart +++ b/packages/flutter/lib/src/material/theme.dart @@ -164,7 +164,7 @@ class Theme extends StatelessWidget { } } -class _InheritedTheme extends InheritedWidget { +class _InheritedTheme extends InheritedTheme { const _InheritedTheme({ Key key, @required this.theme, @@ -174,6 +174,12 @@ class _InheritedTheme extends InheritedWidget { final Theme theme; + @override + Widget wrap(BuildContext context, Widget child) { + final _InheritedTheme ancestorTheme = context.ancestorWidgetOfExactType(_InheritedTheme); + return identical(this, ancestorTheme) ? child : Theme(data: theme.data, child: child); + } + @override bool updateShouldNotify(_InheritedTheme old) => theme.data != old.theme.data; } diff --git a/packages/flutter/lib/src/material/toggle_buttons_theme.dart b/packages/flutter/lib/src/material/toggle_buttons_theme.dart index 7bc27608af..d80b6ed389 100644 --- a/packages/flutter/lib/src/material/toggle_buttons_theme.dart +++ b/packages/flutter/lib/src/material/toggle_buttons_theme.dart @@ -227,7 +227,7 @@ class ToggleButtonsThemeData extends Diagnosticable { /// /// Values specified here are used for [ToggleButtons] properties that are not /// given an explicit non-null value. -class ToggleButtonsTheme extends InheritedWidget { +class ToggleButtonsTheme extends InheritedTheme { /// Creates a toggle buttons theme that controls the color and border /// parameters for [ToggleButtons]. /// @@ -256,6 +256,12 @@ class ToggleButtonsTheme extends InheritedWidget { return toggleButtonsTheme?.data ?? Theme.of(context).toggleButtonsTheme; } + @override + Widget wrap(BuildContext context, Widget child) { + final ToggleButtonsTheme ancestorTheme = context.ancestorWidgetOfExactType(ToggleButtonsTheme); + return identical(this, ancestorTheme) ? child : ToggleButtonsTheme(data: data, child: child); + } + @override bool updateShouldNotify(ToggleButtonsTheme oldWidget) => data != oldWidget.data; } diff --git a/packages/flutter/lib/src/material/tooltip_theme.dart b/packages/flutter/lib/src/material/tooltip_theme.dart index 2976db8cf0..73ff97a0ef 100644 --- a/packages/flutter/lib/src/material/tooltip_theme.dart +++ b/packages/flutter/lib/src/material/tooltip_theme.dart @@ -213,7 +213,7 @@ class TooltipThemeData extends Diagnosticable { /// ), /// ``` /// {@end-tool} -class TooltipTheme extends InheritedWidget { +class TooltipTheme extends InheritedTheme { /// Creates a tooltip theme that controls the configurations for /// [Tooltip]. /// @@ -241,6 +241,12 @@ class TooltipTheme extends InheritedWidget { return tooltipTheme?.data ?? Theme.of(context).tooltipTheme; } + @override + Widget wrap(BuildContext context, Widget child) { + final TooltipTheme ancestorTheme = context.ancestorWidgetOfExactType(TooltipTheme); + return identical(this, ancestorTheme) ? child : TooltipTheme(data: data, child: child); + } + @override bool updateShouldNotify(TooltipTheme oldWidget) => data != oldWidget.data; } diff --git a/packages/flutter/lib/src/widgets/icon_theme.dart b/packages/flutter/lib/src/widgets/icon_theme.dart index e407a295a9..8a922bde09 100644 --- a/packages/flutter/lib/src/widgets/icon_theme.dart +++ b/packages/flutter/lib/src/widgets/icon_theme.dart @@ -7,11 +7,12 @@ import 'package:flutter/foundation.dart'; import 'basic.dart'; import 'framework.dart'; import 'icon_theme_data.dart'; +import 'inherited_theme.dart'; /// Controls the default color, opacity, and size of icons in a widget subtree. /// /// The icon theme is honored by [Icon] and [ImageIcon] widgets. -class IconTheme extends InheritedWidget { +class IconTheme extends InheritedTheme { /// Creates an icon theme that controls the color, opacity, and size of /// descendant widgets. /// @@ -70,6 +71,12 @@ class IconTheme extends InheritedWidget { @override bool updateShouldNotify(IconTheme oldWidget) => data != oldWidget.data; + @override + Widget wrap(BuildContext context, Widget child) { + final IconTheme iconTheme = context.ancestorWidgetOfExactType(IconTheme); + return identical(this, iconTheme) ? child : IconTheme(data: data, child: child); + } + @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); diff --git a/packages/flutter/lib/src/widgets/inherited_theme.dart b/packages/flutter/lib/src/widgets/inherited_theme.dart new file mode 100644 index 0000000000..75d07d4da6 --- /dev/null +++ b/packages/flutter/lib/src/widgets/inherited_theme.dart @@ -0,0 +1,146 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/foundation.dart'; + +import 'framework.dart'; + +/// An [InheritedWidget] that defines visual properties like colors +/// and text styles, which the [child]'s subtree depends on. +/// +/// The [wrap] method is used by [captureAll] to construct a widget +/// that will wrap a child in all of the inherited themes which +/// are present in a build context but are not present in the +/// context that the returned widget is eventually built in. +/// +/// A widget that's shown in a different context from the one it's +/// built in, like the contents of a new route or an overlay, will +/// be able to depend on inherited widget ancestors of the context +/// it's built in. +/// +/// {@tool snippet --template=freeform} +/// This example demonstrates how `InheritedTheme.captureAll()` can be used +/// to wrap the contents of a new route with the inherited themes that +/// are present when the route is built - but are not present when route +/// is actually shown. +/// +/// If the same code is run without `InheritedTheme.captureAll(), the +/// new route's Text widget will inherit the "something must be wrong" +/// fallback text style, rather than the default text style defined in MyApp. +/// +/// ```dart imports +/// import 'package:flutter/material.dart'; +/// ``` +/// +/// ```dart main +/// void main() { +/// runApp(MyApp()); +/// } +/// ``` +/// +/// ```dart +/// class MyAppBody extends StatelessWidget { +/// @override +/// Widget build(BuildContext context) { +/// return GestureDetector( +/// onTap: () { +/// Navigator.of(context).push( +/// MaterialPageRoute( +/// builder: (BuildContext _) { +/// // InheritedTheme.captureAll() saves references to themes that +/// // are found above the context provided to this widget's build +/// // method, notably the DefaultTextStyle defined in MyApp. The +/// // context passed to the MaterialPageRoute's builder is not used, +/// // because its ancestors are above MyApp's home. +/// return InheritedTheme.captureAll(context, Container( +/// alignment: Alignment.center, +/// color: Theme.of(context).colorScheme.surface, +/// child: Text('Hello World'), +/// )); +/// }, +/// ), +/// ); +/// }, +/// child: Center(child: Text('Tap Here')), +/// ); +/// } +/// } +/// +/// class MyApp extends StatelessWidget { +/// @override +/// Widget build(BuildContext context) { +/// return MaterialApp( +/// home: Scaffold( +/// // Override the DefaultTextStyle defined by the Scaffold. +/// // Descendant widgets will inherit this big blue text style. +/// body: DefaultTextStyle( +/// style: TextStyle(fontSize: 48, color: Colors.blue), +/// child: MyAppBody(), +/// ), +/// ), +/// ); +/// } +/// } +/// ``` +/// {@end-tool} +abstract class InheritedTheme extends InheritedWidget { + /// Abstract const constructor. This constructor enables subclasses to provide + /// const constructors so that they can be used in const expressions. + + const InheritedTheme({ + Key key, + @required Widget child, + }) : super(key: key, child: child); + + /// Return a copy of this inherited theme with the specified [child]. + /// + /// If the identical inherited theme is already visible from [context] then + /// just return the [child]. + /// + /// This implementation for [TooltipTheme] is typical: + /// ```dart + /// Widget wrap(BuildContext context, Widget child) { + /// final TooltipTheme ancestorTheme = context.ancestorWidgetOfExactType(TooltipTheme); + /// return identical(this, ancestorTheme) ? child : TooltipTheme(data: data, child: child); + /// } + /// ``` + Widget wrap(BuildContext context, Widget child); + + /// Returns a widget that will [wrap] child in all of the inherited themes + /// which are visible from [context]. + static Widget captureAll(BuildContext context, Widget child) { + assert(child != null); + assert(context != null); + + final List themes = []; + context.visitAncestorElements((Element ancestor) { + if (ancestor is InheritedElement && ancestor.widget is InheritedTheme) { + final InheritedTheme theme = ancestor.widget; + themes.add(theme); + } + return true; + }); + + return _CaptureAll(themes: themes, child: child); + } +} + +class _CaptureAll extends StatelessWidget { + const _CaptureAll({ + Key key, + @required this.themes, + @required this.child + }) : assert(themes != null), assert(child != null), super(key: key); + + final List themes; + final Widget child; + + @override + Widget build(BuildContext context) { + Widget wrappedChild = child; + for (InheritedTheme theme in themes) + wrappedChild = theme.wrap(context, wrappedChild); + return wrappedChild; + } +} diff --git a/packages/flutter/lib/src/widgets/text.dart b/packages/flutter/lib/src/widgets/text.dart index a5f76a4838..0108ffe9e3 100644 --- a/packages/flutter/lib/src/widgets/text.dart +++ b/packages/flutter/lib/src/widgets/text.dart @@ -7,6 +7,7 @@ import 'package:flutter/painting.dart'; import 'basic.dart'; import 'framework.dart'; +import 'inherited_theme.dart'; import 'media_query.dart'; // Examples can assume: @@ -20,7 +21,7 @@ import 'media_query.dart'; /// smoothly over a given duration. /// * [DefaultTextStyleTransition], which takes a provided [Animation] to /// animate changes in text style smoothly over time. -class DefaultTextStyle extends InheritedWidget { +class DefaultTextStyle extends InheritedTheme { /// Creates a default text style for the given subtree. /// /// Consider using [DefaultTextStyle.merge] to inherit styling information @@ -161,6 +162,20 @@ class DefaultTextStyle extends InheritedWidget { textWidthBasis != oldWidget.textWidthBasis; } + @override + Widget wrap(BuildContext context, Widget child) { + final DefaultTextStyle defaultTextStyle = context.ancestorWidgetOfExactType(DefaultTextStyle); + return identical(this, defaultTextStyle) ? child : DefaultTextStyle( + style: style, + textAlign: textAlign, + softWrap: softWrap, + overflow: overflow, + maxLines: maxLines, + textWidthBasis: textWidthBasis, + child: child, + ); + } + @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); diff --git a/packages/flutter/lib/widgets.dart b/packages/flutter/lib/widgets.dart index 8130696055..ac16980dc5 100644 --- a/packages/flutter/lib/widgets.dart +++ b/packages/flutter/lib/widgets.dart @@ -52,6 +52,7 @@ export 'src/widgets/image_icon.dart'; export 'src/widgets/implicit_animations.dart'; export 'src/widgets/inherited_model.dart'; export 'src/widgets/inherited_notifier.dart'; +export 'src/widgets/inherited_theme.dart'; export 'src/widgets/layout_builder.dart'; export 'src/widgets/list_wheel_scroll_view.dart'; export 'src/widgets/localizations.dart'; diff --git a/packages/flutter/test/material/inherited_theme_test.dart b/packages/flutter/test/material/inherited_theme_test.dart new file mode 100644 index 0000000000..128a861ddc --- /dev/null +++ b/packages/flutter/test/material/inherited_theme_test.dart @@ -0,0 +1,723 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../rendering/mock_canvas.dart'; + +void main() { + testWidgets('Theme.wrap()', (WidgetTester tester) async { + const Color primaryColor = Color(0xFF00FF00); + final Key primaryContainerKey = UniqueKey(); + + // Effectively the same as a StatelessWidget subclass. + final Widget primaryBox = Builder( + builder: (BuildContext context) { + return Container( + key: primaryContainerKey, + color: Theme.of(context).primaryColor, + ); + }, + ); + + BuildContext navigatorContext; + + Widget buildFrame() { + return MaterialApp( + home: Scaffold( + body: Builder( // Introduce a context so the app's Theme is visible. + builder: (BuildContext context) { + navigatorContext = context; + return Theme( + data: Theme.of(context).copyWith(primaryColor: primaryColor), + child: Builder( // Introduce a context so the shadow Theme is visible to captureAll(). + builder: (BuildContext context) { + return Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + RaisedButton( + child: const Text('push unwrapped'), + onPressed: () { + Navigator.of(context).push( + MaterialPageRoute( + // The primaryBox will see the default Theme when built. + builder: (BuildContext _) => primaryBox, + ), + ); + }, + ), + RaisedButton( + child: const Text('push wrapped'), + onPressed: () { + Navigator.of(context).push( + MaterialPageRoute( + // Capture the shadow Theme. + builder: (BuildContext _) => InheritedTheme.captureAll(context, primaryBox), + ), + ); + }, + ), + ], + ), + ); + }, + ), + ); + }, + ), + ), + ); + } + + Color containerColor() { + final BoxDecoration decoration = tester.widget(find.byKey(primaryContainerKey)).decoration; + return decoration.color; + } + + await tester.pumpWidget(buildFrame()); + + // Show the route which contains primaryBox which was wrapped with + // InheritedTheme.captureAll(). + await tester.tap(find.text('push wrapped')); + await tester.pumpAndSettle(); // route animation + expect(containerColor(), primaryColor); + + Navigator.of(navigatorContext).pop(); + await tester.pumpAndSettle(); // route animation + + // Show the route which contains primaryBox + await tester.tap(find.text('push unwrapped')); + await tester.pumpAndSettle(); // route animation + expect(containerColor(), isNot(primaryColor)); + }); + + testWidgets('PopupMenuTheme.wrap()', (WidgetTester tester) async { + const double menuFontSize = 24; + const Color menuTextColor = Color(0xFF0000FF); + bool captureInheritedThemes = true; + + Widget buildFrame() { + return MaterialApp( + home: Scaffold( + body: PopupMenuTheme( + data: const PopupMenuThemeData( + // The menu route's elevation, shape, and color are defined by the + // current context, so they're not affected by ThemeData.captureAll(). + textStyle: TextStyle(fontSize: menuFontSize, color: menuTextColor), + ), + child: Center( + child: PopupMenuButton( + // The appearance of the menu items' text is defined by the + // PopupMenuTheme defined above. Popup menus use + // InheritedTheme.captureAll() by default. + captureInheritedThemes: captureInheritedThemes, + child: const Text('show popupmenu'), + onSelected: (int result) { }, + itemBuilder: (BuildContext context) { + return const >[ + PopupMenuItem(value: 1, child: Text('One')), + PopupMenuItem(value: 2, child: Text('Two')), + ]; + }, + ), + ), + ), + ), + ); + } + + TextStyle itemTextStyle(String text) { + return tester.widget( + find.descendant(of: find.text(text), matching: find.byType(RichText)), + ).text.style; + } + + await tester.pumpWidget(buildFrame()); + + await tester.tap(find.text('show popupmenu')); + await tester.pumpAndSettle(); // menu route animation + expect(itemTextStyle('One').fontSize, menuFontSize); + expect(itemTextStyle('One').color, menuTextColor); + expect(itemTextStyle('Two').fontSize, menuFontSize); + expect(itemTextStyle('Two').color, menuTextColor); + + // Dismiss the menu + await tester.tap(find.text('One')); + await tester.pumpAndSettle(); // menu route animation + + // Defeat the default support for capturing the PopupMenuTheme. + captureInheritedThemes = false; + await tester.pumpWidget(buildFrame()); + + await tester.tap(find.text('show popupmenu')); + await tester.pumpAndSettle(); // menu route animation + expect(itemTextStyle('One').fontSize, isNot(menuFontSize)); + expect(itemTextStyle('One').color, isNot(menuTextColor)); + expect(itemTextStyle('Two').fontSize, isNot(menuFontSize)); + expect(itemTextStyle('Two').color, isNot(menuTextColor)); + }); + + testWidgets('BannerTheme.wrap()', (WidgetTester tester) async { + const Color bannerBackgroundColor = Color(0xFF0000FF); + const double bannerFontSize = 48; + const Color bannerTextColor = Color(0xFF00FF00); + + final Widget banner = MaterialBanner( + content: const Text('hello'), + actions: [ + FlatButton( + child: const Text('action'), + onPressed: () { }, + ), + ], + ); + + BuildContext navigatorContext; + + Widget buildFrame() { + return MaterialApp( + home: Scaffold( + body: MaterialBannerTheme( + data: const MaterialBannerThemeData( + backgroundColor: bannerBackgroundColor, + contentTextStyle: TextStyle(fontSize: bannerFontSize, color: bannerTextColor), + ), + child: Builder( // Introduce a context so the shadow BannerTheme is visible to captureAll(). + builder: (BuildContext context) { + navigatorContext = context; + return Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + RaisedButton( + child: const Text('push unwrapped'), + onPressed: () { + Navigator.of(context).push( + MaterialPageRoute( + // The Banner will see the default BannerTheme when built. + builder: (BuildContext _) => banner, + ), + ); + }, + ), + RaisedButton( + child: const Text('push wrapped'), + onPressed: () { + Navigator.of(context).push( + MaterialPageRoute( + // Capture the shadow BannerTheme. + builder: (BuildContext _) => InheritedTheme.captureAll(context, banner), + ), + ); + }, + ), + ], + ), + ); + }, + ), + ), + ), + ); + } + + Color bannerColor() { + final BoxDecoration decoration = tester.widget( + find.descendant(of: find.byType(MaterialBanner), matching: find.byType(Container)).first, + ).decoration; + return decoration.color; + } + + TextStyle getTextStyle(String text) { + return tester.widget( + find.descendant( + of: find.text(text), + matching: find.byType(RichText), + ), + ).text.style; + } + + await tester.pumpWidget(buildFrame()); + + // Show the route which contains the banner. + await tester.tap(find.text('push wrapped')); + await tester.pumpAndSettle(); // route animation + expect(bannerColor(), bannerBackgroundColor); + expect(getTextStyle('hello').fontSize, bannerFontSize); + expect(getTextStyle('hello').color, bannerTextColor); + + Navigator.of(navigatorContext).pop(); + await tester.pumpAndSettle(); // route animation + + await tester.tap(find.text('push unwrapped')); + await tester.pumpAndSettle(); // route animation + expect(bannerColor(), isNot(bannerBackgroundColor)); + expect(getTextStyle('hello').fontSize, isNot(bannerFontSize)); + expect(getTextStyle('hello').color, isNot(bannerTextColor)); + }); + + testWidgets('DividerTheme.wrap()', (WidgetTester tester) async { + const Color dividerColor = Color(0xFF0000FF); + const double dividerSpace = 13; + const double dividerThickness = 7; + const Widget divider = Center(child: Divider()); + + BuildContext navigatorContext; + + Widget buildFrame() { + return MaterialApp( + home: Scaffold( + body: DividerTheme( + data: const DividerThemeData( + color: dividerColor, + space: dividerSpace, + thickness: dividerThickness, + ), + child: Builder( // Introduce a context so the shadow DividerTheme is visible to captureAll(). + builder: (BuildContext context) { + navigatorContext = context; + return Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + RaisedButton( + child: const Text('push unwrapped'), + onPressed: () { + Navigator.of(context).push( + MaterialPageRoute( + // The Banner will see the default BannerTheme when built. + builder: (BuildContext _) => divider, + ), + ); + }, + ), + RaisedButton( + child: const Text('push wrapped'), + onPressed: () { + Navigator.of(context).push( + MaterialPageRoute( + // Capture the shadow BannerTheme. + builder: (BuildContext _) => InheritedTheme.captureAll(context, divider), + ), + ); + }, + ), + ], + ), + ); + }, + ), + ), + ), + ); + } + + BorderSide dividerBorder() { + final BoxDecoration decoration = tester.widget( + find.descendant(of: find.byType(Divider), matching: find.byType(Container)).first, + ).decoration; + return decoration.border.bottom; + } + + await tester.pumpWidget(buildFrame()); + + // Show a route which contains a divider. + await tester.tap(find.text('push wrapped')); + await tester.pumpAndSettle(); // route animation + expect(tester.getSize(find.byType(Divider)).height, dividerSpace); + expect(dividerBorder().color, dividerColor); + expect(dividerBorder().width, dividerThickness); + + Navigator.of(navigatorContext).pop(); + await tester.pumpAndSettle(); // route animation + + await tester.tap(find.text('push unwrapped')); + await tester.pumpAndSettle(); // route animation + expect(tester.getSize(find.byType(Divider)).height, isNot(dividerSpace)); + expect(dividerBorder().color, isNot(dividerColor)); + expect(dividerBorder().width, isNot(dividerThickness)); + }); + + testWidgets('ListTileTheme.wrap()', (WidgetTester tester) async { + const Color tileSelectedColor = Color(0xFF00FF00); + const Color tileIconColor = Color(0xFF0000FF); + const Color tileTextColor = Color(0xFFFF0000); + + final Key selectedIconKey = UniqueKey(); + final Key unselectedIconKey = UniqueKey(); + + final Widget listTiles = Scaffold( + body: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + leading: Icon(Icons.computer, key: selectedIconKey), + title: const Text('selected'), + enabled: true, + selected: true, + ), + ListTile( + leading: Icon(Icons.add, key: unselectedIconKey), + title: const Text('unselected'), + enabled: true, + selected: false, + ), + ], + ), + ), + ); + + BuildContext navigatorContext; + + Widget buildFrame() { + return MaterialApp( + home: Scaffold( + body: ListTileTheme( + selectedColor: tileSelectedColor, + textColor: tileTextColor, + iconColor: tileIconColor, + child: Builder( // Introduce a context so the shadow ListTileTheme is visible to captureAll(). + builder: (BuildContext context) { + navigatorContext = context; + return Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + RaisedButton( + child: const Text('push unwrapped'), + onPressed: () { + Navigator.of(context).push( + MaterialPageRoute( + // The Banner will see the default BannerTheme when built. + builder: (BuildContext _) => listTiles, + ), + ); + }, + ), + RaisedButton( + child: const Text('push wrapped'), + onPressed: () { + Navigator.of(context).push( + MaterialPageRoute( + // Capture the shadow BannerTheme. + builder: (BuildContext _) => InheritedTheme.captureAll(context, listTiles), + ), + ); + }, + ), + ], + ), + ); + }, + ), + ), + ), + ); + } + + TextStyle getTextStyle(String text) { + return tester.widget( + find.descendant(of: find.text(text), matching: find.byType(RichText)), + ).text.style; + } + + TextStyle getIconStyle(Key key) { + return tester.widget( + find.descendant( + of: find.byKey(key), + matching: find.byType(RichText), + ), + ).text.style; + } + + await tester.pumpWidget(buildFrame()); + + // Show a route which contains listTiles. + await tester.tap(find.text('push wrapped')); + await tester.pumpAndSettle(); // route animation + expect(getTextStyle('unselected').color, tileTextColor); + expect(getTextStyle('selected').color, tileSelectedColor); + expect(getIconStyle(selectedIconKey).color, tileSelectedColor); + expect(getIconStyle(unselectedIconKey).color, tileIconColor); + + Navigator.of(navigatorContext).pop(); + await tester.pumpAndSettle(); // route animation + + await tester.tap(find.text('push unwrapped')); + await tester.pumpAndSettle(); // route animation + expect(getTextStyle('unselected').color, isNot(tileTextColor)); + expect(getTextStyle('selected').color, isNot(tileSelectedColor)); + expect(getIconStyle(selectedIconKey).color, isNot(tileSelectedColor)); + expect(getIconStyle(unselectedIconKey).color, isNot(tileIconColor)); + }); + + testWidgets('SliderTheme.wrap()', (WidgetTester tester) async { + const Color activeTrackColor = Color(0xFF00FF00); + const Color inactiveTrackColor = Color(0xFF0000FF); + const Color thumbColor = Color(0xFFFF0000); + + final Widget slider = Scaffold( + body: Center( + child: Slider( + value: 0.5, + onChanged: (double value) { }, + ), + ), + ); + + BuildContext navigatorContext; + + Widget buildFrame() { + return MaterialApp( + home: Scaffold( + body: SliderTheme( + data: const SliderThemeData( + activeTrackColor: activeTrackColor, + inactiveTrackColor: inactiveTrackColor, + thumbColor: thumbColor, + ), + child: Builder( // Introduce a context so the shadow SliderTheme is visible to captureAll(). + builder: (BuildContext context) { + navigatorContext = context; + return Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + RaisedButton( + child: const Text('push unwrapped'), + onPressed: () { + Navigator.of(context).push( + MaterialPageRoute( + // The slider will see the default SliderTheme when built. + builder: (BuildContext _) => slider, + ), + ); + }, + ), + RaisedButton( + child: const Text('push wrapped'), + onPressed: () { + Navigator.of(context).push( + MaterialPageRoute( + // Capture the shadow SliderTheme. + builder: (BuildContext _) => InheritedTheme.captureAll(context, slider), + ), + ); + }, + ), + ], + ), + ); + }, + ), + ), + ), + ); + } + + await tester.pumpWidget(buildFrame()); + + // Show a route which contains listTiles. + await tester.tap(find.text('push wrapped')); + await tester.pumpAndSettle(); // route animation + RenderBox sliderBox = tester.firstRenderObject(find.byType(Slider)); + expect(sliderBox, paints..rect(color: activeTrackColor)..rect(color: inactiveTrackColor)); + expect(sliderBox, paints..circle(color: thumbColor)); + + Navigator.of(navigatorContext).pop(); + await tester.pumpAndSettle(); // route animation + + await tester.tap(find.text('push unwrapped')); + await tester.pumpAndSettle(); // route animation + sliderBox = tester.firstRenderObject(find.byType(Slider)); + expect(sliderBox, isNot(paints..rect(color: activeTrackColor)..rect(color: inactiveTrackColor))); + expect(sliderBox, isNot(paints..circle(color: thumbColor))); + }); + + testWidgets('ToggleButtonsTheme.wrap()', (WidgetTester tester) async { + const Color buttonColor = Color(0xFF00FF00); + const Color selectedButtonColor = Color(0xFFFF0000); + + final Widget toggleButtons = Scaffold( + body: Center( + child: ToggleButtons( + children: const [ + Text('selected'), + Text('unselected'), + ], + isSelected: const [true, false], + onPressed: (int index) { }, + ), + ), + ); + + BuildContext navigatorContext; + + Widget buildFrame() { + return MaterialApp( + home: Scaffold( + body: ToggleButtonsTheme( + data: const ToggleButtonsThemeData( + color: buttonColor, + selectedColor: selectedButtonColor, + ), + child: Builder( // Introduce a context so the shadow ToggleButtonsTheme is visible to captureAll(). + builder: (BuildContext context) { + navigatorContext = context; + return Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + RaisedButton( + child: const Text('push unwrapped'), + onPressed: () { + Navigator.of(context).push( + MaterialPageRoute( + // The slider will see the default ToggleButtonsTheme when built. + builder: (BuildContext _) => toggleButtons, + ), + ); + }, + ), + RaisedButton( + child: const Text('push wrapped'), + onPressed: () { + Navigator.of(context).push( + MaterialPageRoute( + // Capture the shadow toggleButtons. + builder: (BuildContext _) => InheritedTheme.captureAll(context, toggleButtons), + ), + ); + }, + ), + ], + ), + ); + }, + ), + ), + ), + ); + } + + Color getTextColor(String text) { + return tester.widget( + find.descendant(of: find.text(text), matching: find.byType(RichText)), + ).text.style.color; + } + + await tester.pumpWidget(buildFrame()); + + // Show a route which contains toggleButtons. + await tester.tap(find.text('push wrapped')); + await tester.pumpAndSettle(); // route animation + expect(getTextColor('selected'), selectedButtonColor); + expect(getTextColor('unselected'), buttonColor); + + Navigator.of(navigatorContext).pop(); + await tester.pumpAndSettle(); // route animation + + await tester.tap(find.text('push unwrapped')); + await tester.pumpAndSettle(); // route animation + expect(getTextColor('selected'), isNot(selectedButtonColor)); + expect(getTextColor('unselected'), isNot(buttonColor)); + + }); + + testWidgets('ButtonTheme.wrap()', (WidgetTester tester) async { + const Color buttonColor = Color(0xFF00FF00); + const Color disabledButtonColor = Color(0xFFFF0000); + + final Widget buttons = Scaffold( + body: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const RaisedButton(child: Text('disabled'), onPressed: null), + RaisedButton(child: const Text('enabled'), onPressed: () { }), + ], + ), + ), + ); + + BuildContext navigatorContext; + + Widget buildFrame() { + return MaterialApp( + home: Scaffold( + body: ButtonTheme.fromButtonThemeData( + data: const ButtonThemeData( + buttonColor: buttonColor, + disabledColor: disabledButtonColor, + ), + child: Builder( // Introduce a context so the shadow ButtonTheme is visible to captureAll(). + builder: (BuildContext context) { + navigatorContext = context; + return Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + RaisedButton( + child: const Text('push unwrapped'), + onPressed: () { + Navigator.of(context).push( + MaterialPageRoute( + // The slider will see the default ButtonTheme when built. + builder: (BuildContext _) => buttons, + ), + ); + }, + ), + RaisedButton( + child: const Text('push wrapped'), + onPressed: () { + Navigator.of(context).push( + MaterialPageRoute( + // Capture the shadow toggleButtons. + builder: (BuildContext _) => InheritedTheme.captureAll(context, buttons), + ), + ); + }, + ), + ], + ), + ); + }, + ), + ), + ), + ); + } + + Color getButtonColor(String text) { + return tester.widget( + find.descendant( + of: find.widgetWithText(RawMaterialButton, text), + matching: find.byType(Material), + ) + ).color; + } + + await tester.pumpWidget(buildFrame()); + + // Show a route which contains toggleButtons. + await tester.tap(find.text('push wrapped')); + await tester.pumpAndSettle(); // route animation + expect(getButtonColor('disabled'), disabledButtonColor); + expect(getButtonColor('enabled'), buttonColor); + + Navigator.of(navigatorContext).pop(); + await tester.pumpAndSettle(); // route animation + + await tester.tap(find.text('push unwrapped')); + await tester.pumpAndSettle(); // route animation + expect(getButtonColor('disabled'), isNot(disabledButtonColor)); + expect(getButtonColor('enabled'), isNot(buttonColor)); + + }); + +} diff --git a/packages/flutter/test/widgets/inherited_theme_test.dart b/packages/flutter/test/widgets/inherited_theme_test.dart new file mode 100644 index 0000000000..1a56cfa764 --- /dev/null +++ b/packages/flutter/test/widgets/inherited_theme_test.dart @@ -0,0 +1,149 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; + +class TestRoute extends PageRouteBuilder { + TestRoute(Widget child) : super( + pageBuilder: (BuildContext _, Animation __, Animation ___) => child, + ); +} + +class IconTextBox extends StatelessWidget { + const IconTextBox(this.text); + final String text; + @override + Widget build(BuildContext context) { + return Container( + alignment: Alignment.center, + child: Row( + children: [const Icon(IconData(0x41, fontFamily: 'Roboto')), Text(text)], + ), + ); + } +} + +void main() { + testWidgets('InheritedTheme.captureAll()', (WidgetTester tester) async { + const double fontSize = 32; + const double iconSize = 48; + const Color textColor = Color(0xFF00FF00); + const Color iconColor = Color(0xFF0000FF); + bool useCaptureAll = false; + BuildContext navigatorContext; + + Widget buildFrame() { + return WidgetsApp( + color: const Color(0xFFFFFFFF), + onGenerateRoute: (RouteSettings settings) { + return TestRoute( + // The outer DefaultTextStyle and IconTheme widgets must have + // no effect on the test because InheritedTheme.captureAll() + // is required to only save the closest InheritedTheme ancestors. + DefaultTextStyle( + style: const TextStyle(fontSize: iconSize, color: iconColor), + child: IconTheme( + data: const IconThemeData(size: fontSize, color: textColor), + // The inner DefaultTextStyle and IconTheme widgets define + // InheritedThemes that captureAll() will wrap() around + // TestRoute's IconTextBox child. + child: DefaultTextStyle( + style: const TextStyle(fontSize: fontSize, color: textColor), + child: IconTheme( + data: const IconThemeData(size: iconSize, color: iconColor), + child: Builder( + builder: (BuildContext context) { + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + navigatorContext = context; + Navigator.of(context).push( + TestRoute( + useCaptureAll + ? InheritedTheme.captureAll(context, const IconTextBox('Hello')) + : const IconTextBox('Hello') + ), + ); + }, + child: const IconTextBox('Tap'), + ); + }, + ), + ), + ), + ), + ), + ); + }, + ); + } + + TextStyle getIconStyle() { + return tester.widget( + find.descendant( + of: find.byType(Icon), + matching: find.byType(RichText), + ), + ).text.style; + } + + TextStyle getTextStyle(String text) { + return tester.widget( + find.descendant( + of: find.text(text), + matching: find.byType(RichText), + ), + ).text.style; + } + + useCaptureAll = false; + await tester.pumpWidget(buildFrame()); + expect(find.text('Tap'), findsOneWidget); + expect(find.text('Hello'), findsNothing); + expect(getTextStyle('Tap').color, textColor); + expect(getTextStyle('Tap').fontSize, fontSize); + expect(getIconStyle().color, iconColor); + expect(getIconStyle().fontSize, iconSize); + + // Tap to show the TestRoute + await tester.tap(find.text('Tap')); + await tester.pumpAndSettle(); // route transition + expect(find.text('Tap'), findsNothing); + expect(find.text('Hello'), findsOneWidget); + // The new route's text and icons will NOT inherit the DefaultTextStyle or + // IconTheme values. + expect(getTextStyle('Hello').color, isNot(textColor)); + expect(getTextStyle('Hello').fontSize, isNot(fontSize)); + expect(getIconStyle().color, isNot(iconColor)); + expect(getIconStyle().fontSize, isNot(iconSize)); + + // Return to the home route + useCaptureAll = true; + Navigator.of(navigatorContext).pop(); + await tester.pumpAndSettle(); // route transition + + // Verify that all is the same as it was when the test started + expect(find.text('Tap'), findsOneWidget); + expect(find.text('Hello'), findsNothing); + expect(getTextStyle('Tap').color, textColor); + expect(getTextStyle('Tap').fontSize, fontSize); + expect(getIconStyle().color, iconColor); + expect(getIconStyle().fontSize, iconSize); + + // Tap to show the TestRoute. The test route's IconTextBox will have been + // wrapped with InheritedTheme.captureAll(). + await tester.tap(find.text('Tap')); + await tester.pumpAndSettle(); // route transition + expect(find.text('Tap'), findsNothing); + expect(find.text('Hello'), findsOneWidget); + // The new route's text and icons will inherit the DefaultTextStyle or + // IconTheme values because captureAll. + expect(getTextStyle('Hello').color, textColor); + expect(getTextStyle('Hello').fontSize, fontSize); + expect(getIconStyle().color, iconColor); + expect(getIconStyle().fontSize, iconSize); + + }); +}