flutter_flutter/packages/flutter/test/material/inherited_theme_test.dart
Kate Lovett 9d96df2364
Modernize framework lints (#179089)
WIP

Commits separated as follows:
- Update lints in analysis_options files
- Run `dart fix --apply`
- Clean up leftover analysis issues 
- Run `dart format .` in the right places.

Local analysis and testing passes. Checking CI now.

Part of https://github.com/flutter/flutter/issues/178827
- Adoption of flutter_lints in examples/api coming in a separate change
(cc @loic-sharma)

## Pre-launch Checklist

- [ ] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [ ] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [ ] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [ ] I signed the [CLA].
- [ ] I listed at least one issue that this PR fixes in the description
above.
- [ ] I updated/added relevant documentation (doc comments with `///`).
- [ ] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [ ] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [ ] All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel
on [Discord].

**Note**: The Flutter team is currently trialing the use of [Gemini Code
Assist for
GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code).
Comments from the `gemini-code-assist` bot should not be taken as
authoritative feedback from the Flutter team. If you find its comments
useful you can update your code accordingly, but if you are unsure or
disagree with the feedback, please feel free to wait for a Flutter team
member's review for guidance on which automated comments should be
addressed.

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
2025-11-26 01:10:39 +00:00

681 lines
24 KiB
Dart

// Copyright 2014 The Flutter 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';
void main() {
testWidgets('Theme.wrap()', (WidgetTester tester) async {
const 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);
},
);
late 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: <Widget>[
ElevatedButton(
child: const Text('push unwrapped'),
onPressed: () {
Navigator.of(context).push<void>(
MaterialPageRoute<void>(
// The primaryBox will see the default Theme when built.
builder: (BuildContext _) => primaryBox,
),
);
},
),
ElevatedButton(
child: const Text('push wrapped'),
onPressed: () {
Navigator.of(context).push<void>(
MaterialPageRoute<void>(
// Capture the shadow Theme.
builder: (BuildContext _) =>
InheritedTheme.captureAll(context, primaryBox),
),
);
},
),
],
),
);
},
),
);
},
),
),
);
}
Color containerColor() {
return tester.widget<Container>(find.byKey(primaryContainerKey)).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('Material2 - PopupMenuTheme.wrap()', (WidgetTester tester) async {
const double menuFontSize = 24;
const menuTextColor = Color(0xFF0000FF);
Widget buildFrame() {
return MaterialApp(
theme: ThemeData(useMaterial3: false),
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<int>(
// The appearance of the menu items' text is defined by the
// PopupMenuTheme defined above. Popup menus use
// InheritedTheme.captureAll() by default.
child: const Text('show popupmenu'),
onSelected: (int result) {},
itemBuilder: (BuildContext context) {
return const <PopupMenuEntry<int>>[
PopupMenuItem<int>(value: 1, child: Text('One')),
PopupMenuItem<int>(value: 2, child: Text('Two')),
];
},
),
),
),
),
);
}
TextStyle itemTextStyle(String text) {
return tester
.widget<RichText>(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
});
testWidgets('Material3 - PopupMenuTheme.wrap()', (WidgetTester tester) async {
const textStyle = TextStyle(fontSize: 24.0, color: Color(0xFF0000FF));
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().
labelTextStyle: MaterialStatePropertyAll<TextStyle>(textStyle),
),
child: Center(
child: PopupMenuButton<int>(
// The appearance of the menu items' text is defined by the
// PopupMenuTheme defined above. Popup menus use
// InheritedTheme.captureAll() by default.
child: const Text('show popupmenu'),
onSelected: (int result) {},
itemBuilder: (BuildContext context) {
return const <PopupMenuEntry<int>>[
PopupMenuItem<int>(value: 1, child: Text('One')),
PopupMenuItem<int>(value: 2, child: Text('Two')),
];
},
),
),
),
),
);
}
TextStyle itemTextStyle(String text) {
return tester
.widget<RichText>(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, textStyle.fontSize);
expect(itemTextStyle('One').color, textStyle.color);
expect(itemTextStyle('Two').fontSize, textStyle.fontSize);
expect(itemTextStyle('Two').color, textStyle.color);
// Dismiss the menu
await tester.tap(find.text('One'));
await tester.pumpAndSettle(); // menu route animation
});
testWidgets('BannerTheme.wrap()', (WidgetTester tester) async {
const bannerBackgroundColor = Color(0xFF0000FF);
const double bannerFontSize = 48;
const bannerTextColor = Color(0xFF00FF00);
final Widget banner = MaterialBanner(
content: const Text('hello'),
actions: <Widget>[TextButton(child: const Text('action'), onPressed: () {})],
);
late 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: <Widget>[
ElevatedButton(
child: const Text('push unwrapped'),
onPressed: () {
Navigator.of(context).push<void>(
MaterialPageRoute<void>(
// The Banner will see the default BannerTheme when built.
builder: (BuildContext _) => banner,
),
);
},
),
ElevatedButton(
child: const Text('push wrapped'),
onPressed: () {
Navigator.of(context).push<void>(
MaterialPageRoute<void>(
// Capture the shadow BannerTheme.
builder: (BuildContext _) =>
InheritedTheme.captureAll(context, banner),
),
);
},
),
],
),
);
},
),
),
),
);
}
Color bannerColor() {
return tester
.widget<Material>(
find.descendant(of: find.byType(MaterialBanner), matching: find.byType(Material)).first,
)
.color!;
}
TextStyle getTextStyle(String text) {
return tester
.widget<RichText>(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 dividerColor = Color(0xFF0000FF);
const double dividerSpace = 13;
const double dividerThickness = 7;
const Widget divider = Center(child: Divider());
late 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: <Widget>[
ElevatedButton(
child: const Text('push unwrapped'),
onPressed: () {
Navigator.of(context).push<void>(
MaterialPageRoute<void>(
// The Banner will see the default BannerTheme when built.
builder: (BuildContext _) => divider,
),
);
},
),
ElevatedButton(
child: const Text('push wrapped'),
onPressed: () {
Navigator.of(context).push<void>(
MaterialPageRoute<void>(
// Capture the shadow BannerTheme.
builder: (BuildContext _) =>
InheritedTheme.captureAll(context, divider),
),
);
},
),
],
),
);
},
),
),
),
);
}
BorderSide dividerBorder() {
final decoration =
tester
.widget<Container>(
find
.descendant(of: find.byType(Divider), matching: find.byType(Container))
.first,
)
.decoration!
as BoxDecoration;
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 tileSelectedColor = Color(0xFF00FF00);
const tileIconColor = Color(0xFF0000FF);
const tileTextColor = Color(0xFFFF0000);
final Key selectedIconKey = UniqueKey();
final Key unselectedIconKey = UniqueKey();
final Widget listTiles = Scaffold(
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
ListTile(
leading: Icon(Icons.computer, key: selectedIconKey),
title: const Text('selected'),
selected: true,
),
ListTile(
leading: Icon(Icons.add, key: unselectedIconKey),
title: const Text('unselected'),
),
],
),
),
);
late 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: <Widget>[
ElevatedButton(
child: const Text('push unwrapped'),
onPressed: () {
Navigator.of(context).push<void>(
MaterialPageRoute<void>(
// The Banner will see the default BannerTheme when built.
builder: (BuildContext _) => listTiles,
),
);
},
),
ElevatedButton(
child: const Text('push wrapped'),
onPressed: () {
Navigator.of(context).push<void>(
MaterialPageRoute<void>(
// Capture the shadow BannerTheme.
builder: (BuildContext _) =>
InheritedTheme.captureAll(context, listTiles),
),
);
},
),
],
),
);
},
),
),
),
);
}
TextStyle getTextStyle(String text) {
return tester
.widget<RichText>(find.descendant(of: find.text(text), matching: find.byType(RichText)))
.text
.style!;
}
TextStyle getIconStyle(Key key) {
return tester
.widget<RichText>(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 activeTrackColor = Color(0xFF00FF00);
const inactiveTrackColor = Color(0xFF0000FF);
const thumbColor = Color(0xFFFF0000);
final Widget slider = Scaffold(
body: Center(child: Slider(value: 0.5, onChanged: (double value) {})),
);
late 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: <Widget>[
ElevatedButton(
child: const Text('push unwrapped'),
onPressed: () {
Navigator.of(context).push<void>(
MaterialPageRoute<void>(
// The slider will see the default SliderTheme when built.
builder: (BuildContext _) => slider,
),
);
},
),
ElevatedButton(
child: const Text('push wrapped'),
onPressed: () {
Navigator.of(context).push<void>(
MaterialPageRoute<void>(
// 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<RenderBox>(find.byType(Slider));
expect(
sliderBox,
paints
..rrect(color: inactiveTrackColor)
..rrect(color: activeTrackColor),
);
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<RenderBox>(find.byType(Slider));
expect(
sliderBox,
isNot(
paints
..rrect(color: inactiveTrackColor)
..rrect(color: activeTrackColor),
),
);
expect(sliderBox, isNot(paints..circle(color: thumbColor)));
});
testWidgets('ToggleButtonsTheme.wrap()', (WidgetTester tester) async {
const buttonColor = Color(0xFF00FF00);
const selectedButtonColor = Color(0xFFFF0000);
final Widget toggleButtons = Scaffold(
body: Center(
child: ToggleButtons(
isSelected: const <bool>[true, false],
children: const <Widget>[Text('selected'), Text('unselected')],
onPressed: (int index) {},
),
),
);
late 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: <Widget>[
ElevatedButton(
child: const Text('push unwrapped'),
onPressed: () {
Navigator.of(context).push<void>(
MaterialPageRoute<void>(
// The slider will see the default ToggleButtonsTheme when built.
builder: (BuildContext _) => toggleButtons,
),
);
},
),
ElevatedButton(
child: const Text('push wrapped'),
onPressed: () {
Navigator.of(context).push<void>(
MaterialPageRoute<void>(
// Capture the shadow toggleButtons.
builder: (BuildContext _) =>
InheritedTheme.captureAll(context, toggleButtons),
),
);
},
),
],
),
);
},
),
),
),
);
}
Color getTextColor(String text) {
return tester
.widget<RichText>(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));
});
}