chunhtai 2e2d655be8
Relax assertion for adding semantics locale (#178140)
<!--
Thanks for filing a pull request!
Reviewers are typically assigned within a week of filing a request.
To learn more about code review, see our documentation on Tree Hygiene:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
-->

instead forcing a contain: true, uses isCompatibleWith to prevent
implicit merge

## 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-12-09 20:46:18 +00:00

3752 lines
128 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/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import '../widgets/semantics_tester.dart';
import 'app_bar_utils.dart';
TextStyle? _iconStyle(WidgetTester tester, IconData icon) {
final RichText iconRichText = tester.widget<RichText>(
find.descendant(of: find.byIcon(icon).first, matching: find.byType(RichText)),
);
return iconRichText.text.style;
}
void main() {
setUp(() {
debugResetSemanticsIdCounter();
});
testWidgets('AppBar centers title on iOS', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(platform: TargetPlatform.android),
home: Scaffold(appBar: AppBar(title: const Text('X'))),
),
);
final Finder title = find.text('X');
Offset center = tester.getCenter(title);
Size size = tester.getSize(title);
expect(center.dx, lessThan(400 - size.width / 2.0));
for (final platform in <TargetPlatform>[TargetPlatform.iOS, TargetPlatform.macOS]) {
// Clear the widget tree to avoid animating between platforms.
await tester.pumpWidget(Container(key: UniqueKey()));
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(platform: platform),
home: Scaffold(appBar: AppBar(title: const Text('X'))),
),
);
center = tester.getCenter(title);
size = tester.getSize(title);
expect(center.dx, greaterThan(400 - size.width / 2.0), reason: 'on ${platform.name}');
expect(center.dx, lessThan(400 + size.width / 2.0), reason: 'on ${platform.name}');
// One action is still centered.
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(platform: platform),
home: Scaffold(
appBar: AppBar(title: const Text('X'), actions: const <Widget>[Icon(Icons.thumb_up)]),
),
),
);
center = tester.getCenter(title);
size = tester.getSize(title);
expect(center.dx, greaterThan(400 - size.width / 2.0), reason: 'on ${platform.name}');
expect(center.dx, lessThan(400 + size.width / 2.0), reason: 'on ${platform.name}');
// Two actions is left aligned again.
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(platform: platform),
home: Scaffold(
appBar: AppBar(
title: const Text('X'),
actions: const <Widget>[Icon(Icons.thumb_up), Icon(Icons.thumb_up)],
),
),
),
);
center = tester.getCenter(title);
size = tester.getSize(title);
expect(center.dx, lessThan(400 - size.width / 2.0), reason: 'on ${platform.name}');
}
});
testWidgets('AppBar centerTitle:true centers on Android', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(platform: TargetPlatform.android),
home: Scaffold(appBar: AppBar(centerTitle: true, title: const Text('X'))),
),
);
final Finder title = find.text('X');
final Offset center = tester.getCenter(title);
final Size size = tester.getSize(title);
expect(center.dx, greaterThan(400 - size.width / 2.0));
expect(center.dx, lessThan(400 + size.width / 2.0));
});
testWidgets('AppBar centerTitle:false title start edge is 16.0 (LTR)', (
WidgetTester tester,
) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
appBar: AppBar(centerTitle: false, title: const Placeholder(key: Key('X'))),
),
),
);
final Finder titleWidget = find.byKey(const Key('X'));
expect(tester.getTopLeft(titleWidget).dx, 16.0);
expect(tester.getTopRight(titleWidget).dx, 800 - 16.0);
});
testWidgets('AppBar centerTitle:false title start edge is 16.0 (RTL)', (
WidgetTester tester,
) async {
await tester.pumpWidget(
MaterialApp(
home: Directionality(
textDirection: TextDirection.rtl,
child: Scaffold(
appBar: AppBar(centerTitle: false, title: const Placeholder(key: Key('X'))),
),
),
),
);
final Finder titleWidget = find.byKey(const Key('X'));
expect(tester.getTopRight(titleWidget).dx, 800.0 - 16.0);
expect(tester.getTopLeft(titleWidget).dx, 16.0);
});
testWidgets('AppBar titleSpacing:32 title start edge is 32.0 (LTR)', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
appBar: AppBar(
centerTitle: false,
titleSpacing: 32.0,
title: const Placeholder(key: Key('X')),
),
),
),
);
final Finder titleWidget = find.byKey(const Key('X'));
expect(tester.getTopLeft(titleWidget).dx, 32.0);
expect(tester.getTopRight(titleWidget).dx, 800 - 32.0);
});
testWidgets('AppBar titleSpacing:32 title start edge is 32.0 (RTL)', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Directionality(
textDirection: TextDirection.rtl,
child: Scaffold(
appBar: AppBar(
centerTitle: false,
titleSpacing: 32.0,
title: const Placeholder(key: Key('X')),
),
),
),
),
);
final Finder titleWidget = find.byKey(const Key('X'));
expect(tester.getTopRight(titleWidget).dx, 800.0 - 32.0);
expect(tester.getTopLeft(titleWidget).dx, 32.0);
});
testWidgets('AppBar centerTitle:false leading button title left edge is 72.0 (LTR)', (
WidgetTester tester,
) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
appBar: AppBar(centerTitle: false, title: const Text('X')),
// A drawer causes a leading hamburger.
drawer: const Drawer(),
),
),
);
expect(tester.getTopLeft(find.text('X')).dx, 72.0);
});
testWidgets('AppBar centerTitle:false leading button title left edge is 72.0 (RTL)', (
WidgetTester tester,
) async {
await tester.pumpWidget(
MaterialApp(
home: Directionality(
textDirection: TextDirection.rtl,
child: Scaffold(
appBar: AppBar(centerTitle: false, title: const Text('X')),
// A drawer causes a leading hamburger.
drawer: const Drawer(),
),
),
),
);
expect(tester.getTopRight(find.text('X')).dx, 800.0 - 72.0);
});
testWidgets('AppBar centerTitle:false title overflow OK', (WidgetTester tester) async {
// The app bar's title should be constrained to fit within the available space
// between the leading and actions widgets.
final Key titleKey = UniqueKey();
Widget leading = Container();
var actions = <Widget>[];
Widget buildApp() {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
leading: leading,
centerTitle: false,
title: Container(
key: titleKey,
constraints: BoxConstraints.loose(const Size(1000.0, 1000.0)),
),
actions: actions,
),
),
);
}
await tester.pumpWidget(buildApp());
final Finder title = find.byKey(titleKey);
expect(tester.getTopLeft(title).dx, 72.0);
expect(
tester.getSize(title).width,
equals(
800.0 // Screen width.
-
56.0 // Leading button width.
-
16.0 // Leading button to title padding.
-
16.0, // Title right side padding.
),
);
actions = <Widget>[const SizedBox(width: 100.0), const SizedBox(width: 100.0)];
await tester.pumpWidget(buildApp());
expect(tester.getTopLeft(title).dx, 72.0);
// The title shrinks by 200.0 to allow for the actions widgets.
expect(
tester.getSize(title).width,
equals(
800.0 // Screen width.
-
56.0 // Leading button width.
-
16.0 // Leading button to title padding.
-
16.0 // Title to actions padding
-
200.0,
),
); // Actions' width.
leading = Container(); // AppBar will constrain the width to 24.0
await tester.pumpWidget(buildApp());
expect(tester.getTopLeft(title).dx, 72.0);
// Adding a leading widget shouldn't effect the title's size
expect(tester.getSize(title).width, equals(800.0 - 56.0 - 16.0 - 16.0 - 200.0));
});
testWidgets('AppBar centerTitle:true title overflow OK (LTR)', (WidgetTester tester) async {
// The app bar's title should be constrained to fit within the available space
// between the leading and actions widgets. When it's also centered it may
// also be start or end justified if it doesn't fit in the overall center.
final Key titleKey = UniqueKey();
var titleWidth = 700.0;
Widget? leading = Container();
var actions = <Widget>[];
Widget buildApp() {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
leading: leading,
centerTitle: true,
title: Container(
key: titleKey,
constraints: BoxConstraints.loose(Size(titleWidth, 1000.0)),
),
actions: actions,
),
),
);
}
// Centering a title with width 700 within the 800 pixel wide test widget
// would mean that its start edge would have to be 50. The material spec says
// that the start edge of the title must be at least 72.
await tester.pumpWidget(buildApp());
final Finder title = find.byKey(titleKey);
expect(tester.getTopLeft(title).dx, 72.0);
expect(tester.getSize(title).width, equals(700.0));
// Centering a title with width 620 within the 800 pixel wide test widget
// would mean that its start edge would have to be 90. We reserve 72
// on the start and the padded actions occupy 96 on the end. That
// leaves 632, so the title is end justified but its width isn't changed.
await tester.pumpWidget(buildApp());
leading = null;
titleWidth = 620.0;
actions = <Widget>[const SizedBox(width: 48.0), const SizedBox(width: 48.0)];
await tester.pumpWidget(buildApp());
expect(tester.getTopLeft(title).dx, 800 - 620 - 48 - 48 - 16);
expect(tester.getSize(title).width, equals(620.0));
});
testWidgets('AppBar centerTitle:true title overflow OK (RTL)', (WidgetTester tester) async {
// The app bar's title should be constrained to fit within the available space
// between the leading and actions widgets. When it's also centered it may
// also be start or end justified if it doesn't fit in the overall center.
final Key titleKey = UniqueKey();
var titleWidth = 700.0;
Widget? leading = Container();
var actions = <Widget>[];
Widget buildApp() {
return MaterialApp(
home: Directionality(
textDirection: TextDirection.rtl,
child: Scaffold(
appBar: AppBar(
leading: leading,
centerTitle: true,
title: Container(
key: titleKey,
constraints: BoxConstraints.loose(Size(titleWidth, 1000.0)),
),
actions: actions,
),
),
),
);
}
// Centering a title with width 700 within the 800 pixel wide test widget
// would mean that its start edge would have to be 50. The material spec says
// that the start edge of the title must be at least 72.
await tester.pumpWidget(buildApp());
final Finder title = find.byKey(titleKey);
expect(tester.getTopRight(title).dx, 800.0 - 72.0);
expect(tester.getSize(title).width, equals(700.0));
// Centering a title with width 620 within the 800 pixel wide test widget
// would mean that its start edge would have to be 90. We reserve 72
// on the start and the padded actions occupy 96 on the end. That
// leaves 632, so the title is end justified but its width isn't changed.
await tester.pumpWidget(buildApp());
leading = null;
titleWidth = 620.0;
actions = <Widget>[const SizedBox(width: 48.0), const SizedBox(width: 48.0)];
await tester.pumpWidget(buildApp());
expect(tester.getTopRight(title).dx, 620 + 48 + 48 + 16);
expect(tester.getSize(title).width, equals(620.0));
});
testWidgets('AppBar with no Scaffold', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: SizedBox(
height: kToolbarHeight,
child: AppBar(
leading: const Text('L'),
title: const Text('No Scaffold'),
actions: const <Widget>[Text('A1'), Text('A2')],
),
),
),
);
expect(find.text('L'), findsOneWidget);
expect(find.text('No Scaffold'), findsOneWidget);
expect(find.text('A1'), findsOneWidget);
expect(find.text('A2'), findsOneWidget);
});
testWidgets('AppBar render at zero size', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Center(
child: SizedBox.shrink(
child: Scaffold(appBar: AppBar(title: const Text('X'))),
),
),
),
);
final Finder title = find.text('X');
expect(tester.getSize(title).isEmpty, isTrue);
});
testWidgets('AppBar actions are vertically centered', (WidgetTester tester) async {
final appBarKey = UniqueKey();
final leadingKey = UniqueKey();
final titleKey = UniqueKey();
final action0Key = UniqueKey();
final action1Key = UniqueKey();
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
appBar: AppBar(
key: appBarKey,
leading: SizedBox(key: leadingKey, height: 50.0),
title: SizedBox(key: titleKey, height: 40.0),
actions: <Widget>[
SizedBox(key: action0Key, height: 20.0),
SizedBox(key: action1Key, height: 30.0),
],
),
),
),
);
// The vertical center of the widget with key, in global coordinates.
double yCenter(Key key) => tester.getCenter(find.byKey(key)).dy;
expect(yCenter(appBarKey), equals(yCenter(leadingKey)));
expect(yCenter(appBarKey), equals(yCenter(titleKey)));
expect(yCenter(appBarKey), equals(yCenter(action0Key)));
expect(yCenter(appBarKey), equals(yCenter(action1Key)));
});
testWidgets('AppBar drawer icon has default size', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('Howdy!')),
drawer: const Drawer(),
),
),
);
final double iconSize = const IconThemeData.fallback().size!;
expect(tester.getSize(find.byIcon(Icons.menu)), equals(Size(iconSize, iconSize)));
});
testWidgets('Material3 - AppBar drawer icon has default color', (WidgetTester tester) async {
final themeData = ThemeData.from(colorScheme: const ColorScheme.light());
await tester.pumpWidget(
MaterialApp(
theme: themeData,
home: Scaffold(
appBar: AppBar(title: const Text('Howdy!')),
drawer: const Drawer(),
),
),
);
expect(_iconStyle(tester, Icons.menu)?.color, themeData.colorScheme.onSurfaceVariant);
});
testWidgets('AppBar drawer icon is sized by iconTheme', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('Howdy!'), iconTheme: const IconThemeData(size: 30)),
drawer: const Drawer(),
),
),
);
expect(tester.getSize(find.byIcon(Icons.menu)), equals(const Size(30, 30)));
});
testWidgets('AppBar drawer icon is colored by iconTheme', (WidgetTester tester) async {
final themeData = ThemeData.from(colorScheme: const ColorScheme.light());
const color = Color(0xFF2196F3);
await tester.pumpWidget(
MaterialApp(
theme: themeData,
home: Scaffold(
appBar: AppBar(
title: const Text('Howdy!'),
iconTheme: const IconThemeData(color: color),
),
drawer: const Drawer(),
),
),
);
expect(_iconStyle(tester, Icons.menu)?.color, color);
});
testWidgets('AppBar endDrawer icon has default size', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('Howdy!')),
endDrawer: const Drawer(),
),
),
);
final double iconSize = const IconThemeData.fallback().size!;
expect(tester.getSize(find.byIcon(Icons.menu)), equals(Size(iconSize, iconSize)));
});
testWidgets('Material3 - AppBar endDrawer icon has default color', (WidgetTester tester) async {
final themeData = ThemeData.from(colorScheme: const ColorScheme.light());
await tester.pumpWidget(
MaterialApp(
theme: themeData,
home: Scaffold(
appBar: AppBar(title: const Text('Howdy!')),
endDrawer: const Drawer(),
),
),
);
expect(_iconStyle(tester, Icons.menu)?.color, themeData.colorScheme.onSurfaceVariant);
});
testWidgets('AppBar endDrawer icon is sized by iconTheme', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('Howdy!'), iconTheme: const IconThemeData(size: 30)),
endDrawer: const Drawer(),
),
),
);
expect(tester.getSize(find.byIcon(Icons.menu)), equals(const Size(30, 30)));
});
testWidgets('AppBar endDrawer icon is colored by iconTheme', (WidgetTester tester) async {
final themeData = ThemeData.from(colorScheme: const ColorScheme.light());
const color = Color(0xFF2196F3);
await tester.pumpWidget(
MaterialApp(
theme: themeData,
home: Scaffold(
appBar: AppBar(
title: const Text('Howdy!'),
iconTheme: const IconThemeData(color: color),
),
endDrawer: const Drawer(),
),
),
);
expect(_iconStyle(tester, Icons.menu)?.color, color);
});
testWidgets('Material3 - leading widget extends to edge and is square', (
WidgetTester tester,
) async {
final themeData = ThemeData(platform: TargetPlatform.android);
await tester.pumpWidget(
MaterialApp(
theme: themeData,
home: Scaffold(
appBar: AppBar(
leading: IconButton(icon: const Icon(Icons.menu), onPressed: () {}),
title: const Text('X'),
),
drawer: const Column(), // Doesn't really matter. Triggers a hamburger regardless.
),
),
);
// Default IconButton has a size of (48x48).
final Finder hamburger = find.byType(IconButton);
expect(tester.getTopLeft(hamburger), const Offset(4.0, 4.0));
expect(tester.getSize(hamburger), const Size(48.0, 48.0));
await tester.pumpWidget(
MaterialApp(
theme: themeData,
home: Scaffold(
appBar: AppBar(leading: Container(), title: const Text('X')),
),
),
);
// Default leading widget has a size of (56x56).
final Finder leadingBox = find.byType(Container);
expect(tester.getTopLeft(leadingBox), Offset.zero);
expect(tester.getSize(leadingBox), const Size(56.0, 56.0));
// The custom leading widget should still be 56x56 even if its size is smaller.
await tester.pumpWidget(
MaterialApp(
theme: themeData,
home: Scaffold(
appBar: AppBar(
leading: const SizedBox(height: 36, width: 36),
title: const Text('X'),
), // Doesn't really matter. Triggers a hamburger regardless.
),
),
);
final Finder leading = find.byType(SizedBox);
expect(tester.getTopLeft(leading), Offset.zero);
expect(tester.getSize(leading), const Size(56.0, 56.0));
});
testWidgets('Material3 - Action is 4dp from edge and 48dp min', (WidgetTester tester) async {
final theme = ThemeData(platform: TargetPlatform.android);
await tester.pumpWidget(
MaterialApp(
theme: theme,
home: Scaffold(
appBar: AppBar(
title: const Text('X'),
actions: const <Widget>[
IconButton(
icon: Icon(Icons.share),
onPressed: null,
tooltip: 'Share',
iconSize: 20.0,
),
IconButton(icon: Icon(Icons.add), onPressed: null, tooltip: 'Add', iconSize: 60.0),
],
),
),
),
);
final Finder addButton = find.widgetWithIcon(IconButton, Icons.add);
expect(tester.getTopRight(addButton), const Offset(800.0, 0.0));
// It's still the size it was plus the 2 * 8dp padding from IconButton.
expect(tester.getSize(addButton), const Size(60.0 + 2 * 8.0, 56.0));
final Finder shareButton = find.widgetWithIcon(IconButton, Icons.share);
// The 20dp icon is expanded to fill the IconButton's touch target to 48dp.
expect(tester.getSize(shareButton), const Size(48.0, 48.0));
});
testWidgets('Material3 - AppBar uses the specified elevation or defaults to 0', (
WidgetTester tester,
) async {
Widget buildAppBar([double? elevation]) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('Title'), elevation: elevation),
),
);
}
Material getMaterial() => tester.widget<Material>(
find.descendant(of: find.byType(AppBar), matching: find.byType(Material)),
);
// Default elevation should be used for the material.
await tester.pumpWidget(buildAppBar());
expect(getMaterial().elevation, 0);
// AppBar should use the specified elevation.
await tester.pumpWidget(buildAppBar(8.0));
expect(getMaterial().elevation, 8.0);
});
testWidgets('scrolledUnderElevation', (WidgetTester tester) async {
Widget buildAppBar({double? elevation, double? scrolledUnderElevation}) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text('Title'),
elevation: elevation,
scrolledUnderElevation: scrolledUnderElevation,
),
body: ListView.builder(
itemCount: 100,
itemBuilder: (BuildContext context, int index) => ListTile(title: Text('Item $index')),
),
),
);
}
Material getMaterial() => tester.widget<Material>(
find.descendant(of: find.byType(AppBar), matching: find.byType(Material)),
);
await tester.pumpWidget(buildAppBar(elevation: 2, scrolledUnderElevation: 10));
// Starts with the base elevation.
expect(getMaterial().elevation, 2);
await tester.fling(find.text('Item 2'), const Offset(0.0, -600.0), 2000.0);
await tester.pumpAndSettle();
// After scrolling it should be the scrolledUnderElevation.
expect(getMaterial().elevation, 10);
});
testWidgets('Material3 - scrolledUnderElevation with nested scroll view', (
WidgetTester tester,
) async {
Widget buildAppBar({double? scrolledUnderElevation}) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text('Title'),
scrolledUnderElevation: scrolledUnderElevation,
notificationPredicate: (ScrollNotification notification) {
return notification.depth == 1;
},
),
body: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: 4,
itemBuilder: (BuildContext context, int index) {
return SizedBox(
height: 600.0,
width: 800.0,
child: ListView.builder(
itemCount: 100,
itemBuilder: (BuildContext context, int index) =>
ListTile(title: Text('Item $index')),
),
);
},
),
),
);
}
Material getMaterial() => tester.widget<Material>(
find.descendant(of: find.byType(AppBar), matching: find.byType(Material)),
);
await tester.pumpWidget(buildAppBar(scrolledUnderElevation: 10));
// Starts with the base elevation.
expect(getMaterial().elevation, 0.0);
await tester.fling(find.text('Item 2'), const Offset(0.0, -600.0), 2000.0);
await tester.pumpAndSettle();
// After scrolling it should be the scrolledUnderElevation.
expect(getMaterial().elevation, 10);
});
testWidgets('AppBar dimensions, with and without bottom, primary', (WidgetTester tester) async {
const topPadding100 = MediaQueryData(padding: EdgeInsets.only(top: 100.0));
await tester.pumpWidget(
Localizations(
locale: const Locale('en', 'US'),
delegates: const <LocalizationsDelegate<dynamic>>[
DefaultMaterialLocalizations.delegate,
DefaultWidgetsLocalizations.delegate,
],
child: Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: topPadding100,
child: Scaffold(primary: false, appBar: AppBar()),
),
),
),
);
expect(appBarTop(tester), 0.0);
expect(appBarHeight(tester), kToolbarHeight);
await tester.pumpWidget(
Localizations(
locale: const Locale('en', 'US'),
delegates: const <LocalizationsDelegate<dynamic>>[
DefaultMaterialLocalizations.delegate,
DefaultWidgetsLocalizations.delegate,
],
child: Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: topPadding100,
child: Scaffold(appBar: AppBar(title: const Text('title'))),
),
),
),
);
expect(appBarTop(tester), 0.0);
expect(tester.getTopLeft(find.text('title')).dy, greaterThan(100.0));
expect(appBarHeight(tester), kToolbarHeight + 100.0);
await tester.pumpWidget(
Localizations(
locale: const Locale('en', 'US'),
delegates: const <LocalizationsDelegate<dynamic>>[
DefaultMaterialLocalizations.delegate,
DefaultWidgetsLocalizations.delegate,
],
child: Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: topPadding100,
child: Scaffold(
primary: false,
appBar: AppBar(
bottom: PreferredSize(
preferredSize: const Size.fromHeight(200.0),
child: Container(),
),
),
),
),
),
),
);
expect(appBarTop(tester), 0.0);
expect(appBarHeight(tester), kToolbarHeight + 200.0);
await tester.pumpWidget(
Localizations(
locale: const Locale('en', 'US'),
delegates: const <LocalizationsDelegate<dynamic>>[
DefaultMaterialLocalizations.delegate,
DefaultWidgetsLocalizations.delegate,
],
child: Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: topPadding100,
child: Scaffold(
appBar: AppBar(
bottom: PreferredSize(
preferredSize: const Size.fromHeight(200.0),
child: Container(),
),
),
),
),
),
),
);
expect(appBarTop(tester), 0.0);
expect(appBarHeight(tester), kToolbarHeight + 100.0 + 200.0);
await tester.pumpWidget(
Localizations(
locale: const Locale('en', 'US'),
delegates: const <LocalizationsDelegate<dynamic>>[
DefaultMaterialLocalizations.delegate,
DefaultWidgetsLocalizations.delegate,
],
child: Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: topPadding100,
child: AppBar(primary: false, title: const Text('title')),
),
),
),
);
expect(appBarTop(tester), 0.0);
expect(tester.getTopLeft(find.text('title')).dy, lessThan(100.0));
});
testWidgets('AppBar in body excludes bottom SafeArea padding', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/26163
await tester.pumpWidget(
Localizations(
locale: const Locale('en', 'US'),
delegates: const <LocalizationsDelegate<dynamic>>[
DefaultMaterialLocalizations.delegate,
DefaultWidgetsLocalizations.delegate,
],
child: Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: const MediaQueryData(padding: EdgeInsets.symmetric(vertical: 100.0)),
child: Scaffold(
body: Column(children: <Widget>[AppBar(title: const Text('title'))]),
),
),
),
),
);
expect(appBarTop(tester), 0.0);
expect(appBarHeight(tester), kToolbarHeight + 100.0);
});
testWidgets('AppBar.title sees the correct padding from MediaQuery', (WidgetTester tester) async {
var titleBuilt = false;
await tester.pumpWidget(
Localizations(
locale: const Locale('en', 'US'),
delegates: const <LocalizationsDelegate<dynamic>>[
DefaultMaterialLocalizations.delegate,
DefaultWidgetsLocalizations.delegate,
],
child: Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: const MediaQueryData(padding: EdgeInsets.fromLTRB(12, 34, 56, 78)),
child: Scaffold(
appBar: AppBar(
title: Builder(
builder: (BuildContext context) {
titleBuilt = true;
final EdgeInsets padding = MediaQuery.paddingOf(context);
expect(padding, EdgeInsets.zero);
return const Text('heh');
},
),
),
),
),
),
),
);
expect(titleBuilt, isTrue);
});
testWidgets('AppBar updates when you add a drawer', (WidgetTester tester) async {
await tester.pumpWidget(MaterialApp(home: Scaffold(appBar: AppBar())));
expect(find.byIcon(Icons.menu), findsNothing);
await tester.pumpWidget(
MaterialApp(
home: Scaffold(drawer: const Drawer(), appBar: AppBar()),
),
);
expect(find.byIcon(Icons.menu), findsOneWidget);
});
testWidgets('AppBar does not draw menu for drawer if automaticallyImplyLeading is false', (
WidgetTester tester,
) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(drawer: const Drawer(), appBar: AppBar(automaticallyImplyLeading: false)),
),
);
expect(find.byIcon(Icons.menu), findsNothing);
});
testWidgets(
'AppBar does not draw menu for end drawer if automaticallyImplyActions is false and actions is null',
(WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
endDrawer: const Drawer(),
appBar: AppBar(automaticallyImplyActions: false),
),
),
);
expect(find.byIcon(Icons.menu), findsNothing);
},
);
testWidgets(
'AppBar draws menu for end drawer if automaticallyImplyActions is true (default) and actions is null',
(WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(endDrawer: const Drawer(), appBar: AppBar()),
),
);
expect(find.byIcon(Icons.menu), findsOneWidget);
},
);
testWidgets(
'AppBar does not draw menu for end drawer if automaticallyImplyActions is true (default) but actions are explicitly provided',
(WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
endDrawer: const Drawer(),
appBar: AppBar(actions: const <Widget>[Icon(Icons.settings)]),
),
),
);
expect(find.byIcon(Icons.menu), findsNothing);
expect(find.byIcon(Icons.settings), findsOneWidget);
},
);
testWidgets('AppBar does not update the leading if a route is popped case 1', (
WidgetTester tester,
) async {
final Page<void> page1 = MaterialPage<void>(
key: const ValueKey<String>('1'),
child: Scaffold(key: const ValueKey<String>('1'), appBar: AppBar()),
);
final Page<void> page2 = MaterialPage<void>(
key: const ValueKey<String>('2'),
child: Scaffold(key: const ValueKey<String>('2'), appBar: AppBar()),
);
var pages = <Page<void>>[page1];
await tester.pumpWidget(
MaterialApp(
home: Navigator(pages: pages, onPopPage: (Route<dynamic> route, dynamic result) => false),
),
);
expect(find.byType(BackButton), findsNothing);
// Update pages
pages = <Page<void>>[page2];
await tester.pumpWidget(
MaterialApp(
home: Navigator(pages: pages, onPopPage: (Route<dynamic> route, dynamic result) => false),
),
);
expect(find.byType(BackButton), findsNothing);
});
testWidgets('AppBar does not update the leading if a route is popped case 2', (
WidgetTester tester,
) async {
final Page<void> page1 = MaterialPage<void>(
key: const ValueKey<String>('1'),
child: Scaffold(key: const ValueKey<String>('1'), appBar: AppBar()),
);
final Page<void> page2 = MaterialPage<void>(
key: const ValueKey<String>('2'),
child: Scaffold(key: const ValueKey<String>('2'), appBar: AppBar()),
);
var pages = <Page<void>>[page1, page2];
await tester.pumpWidget(
MaterialApp(
home: Navigator(pages: pages, onPopPage: (Route<dynamic> route, dynamic result) => false),
),
);
// The page2 should have a back button
expect(
find.descendant(
of: find.byKey(const ValueKey<String>('2')),
matching: find.byType(BackButton),
),
findsOneWidget,
);
// Update pages
pages = <Page<void>>[page1];
await tester.pumpWidget(
MaterialApp(
home: Navigator(pages: pages, onPopPage: (Route<dynamic> route, dynamic result) => false),
),
);
await tester.pump(const Duration(milliseconds: 10));
// The back button should persist during the pop animation.
expect(
find.descendant(
of: find.byKey(const ValueKey<String>('2')),
matching: find.byType(BackButton),
),
findsOneWidget,
);
});
testWidgets('Material3 - AppBar ink splash draw on the correct canvas', (
WidgetTester tester,
) async {
// This is a regression test for https://github.com/flutter/flutter/issues/58665
final Key key = UniqueKey();
await tester.pumpWidget(
MaterialApp(
// Test was designed against InkSplash so need to make sure that is used.
theme: ThemeData(splashFactory: InkSplash.splashFactory),
home: Center(
child: AppBar(
title: const Text('Abc'),
actions: <Widget>[
IconButton(
key: key,
icon: const Icon(Icons.add_circle),
tooltip: 'First button',
onPressed: () {},
),
],
flexibleSpace: DecoratedBox(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: const Alignment(-0.04, 1.0),
colors: <Color>[Colors.blue.shade500, Colors.blue.shade800],
),
),
),
),
),
),
);
final RenderObject painter = tester.renderObject(
find.descendant(
of: find.descendant(of: find.byType(AppBar), matching: find.byType(Stack)),
matching: find.byType(Material).last,
),
);
await tester.tap(find.byKey(key));
expect(
painter,
paints
..save()
..translate()
..save()
..translate()
..circle(x: 20.0, y: 20.0),
);
});
testWidgets('AppBar handles loose children 0', (WidgetTester tester) async {
final GlobalKey key = GlobalKey();
await tester.pumpWidget(
MaterialApp(
home: Center(
child: AppBar(
leading: Placeholder(key: key),
title: const Text('Abc'),
actions: const <Widget>[
Placeholder(fallbackWidth: 10.0),
Placeholder(fallbackWidth: 10.0),
Placeholder(fallbackWidth: 10.0),
],
),
),
),
);
expect(tester.renderObject<RenderBox>(find.byKey(key)).localToGlobal(Offset.zero), Offset.zero);
expect(tester.renderObject<RenderBox>(find.byKey(key)).size, const Size(56.0, 56.0));
});
testWidgets('AppBar handles loose children 1', (WidgetTester tester) async {
final GlobalKey key = GlobalKey();
await tester.pumpWidget(
MaterialApp(
home: Center(
child: AppBar(
leading: Placeholder(key: key),
title: const Text('Abc'),
actions: const <Widget>[
Placeholder(fallbackWidth: 10.0),
Placeholder(fallbackWidth: 10.0),
Placeholder(fallbackWidth: 10.0),
],
flexibleSpace: DecoratedBox(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: const Alignment(-0.04, 1.0),
colors: <Color>[Colors.blue.shade500, Colors.blue.shade800],
),
),
),
),
),
),
);
expect(tester.renderObject<RenderBox>(find.byKey(key)).localToGlobal(Offset.zero), Offset.zero);
expect(tester.renderObject<RenderBox>(find.byKey(key)).size, const Size(56.0, 56.0));
});
testWidgets('AppBar handles loose children 2', (WidgetTester tester) async {
final GlobalKey key = GlobalKey();
await tester.pumpWidget(
MaterialApp(
home: Center(
child: AppBar(
leading: Placeholder(key: key),
title: const Text('Abc'),
actions: const <Widget>[
Placeholder(fallbackWidth: 10.0),
Placeholder(fallbackWidth: 10.0),
Placeholder(fallbackWidth: 10.0),
],
flexibleSpace: DecoratedBox(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: const Alignment(-0.04, 1.0),
colors: <Color>[Colors.blue.shade500, Colors.blue.shade800],
),
),
),
bottom: PreferredSize(
preferredSize: const Size(0.0, kToolbarHeight),
child: Container(
height: 50.0,
padding: const EdgeInsets.all(4.0),
child: const Placeholder(color: Color(0xFFFFFFFF)),
),
),
),
),
),
);
expect(tester.renderObject<RenderBox>(find.byKey(key)).localToGlobal(Offset.zero), Offset.zero);
expect(tester.renderObject<RenderBox>(find.byKey(key)).size, const Size(56.0, 56.0));
});
testWidgets('AppBar handles loose children 3', (WidgetTester tester) async {
final GlobalKey key = GlobalKey();
await tester.pumpWidget(
MaterialApp(
home: Center(
child: AppBar(
leading: Placeholder(key: key),
title: const Text('Abc'),
actions: const <Widget>[
Placeholder(fallbackWidth: 10.0),
Placeholder(fallbackWidth: 10.0),
Placeholder(fallbackWidth: 10.0),
],
bottom: PreferredSize(
preferredSize: const Size(0.0, kToolbarHeight),
child: Container(
height: 50.0,
padding: const EdgeInsets.all(4.0),
child: const Placeholder(color: Color(0xFFFFFFFF)),
),
),
),
),
),
);
expect(tester.renderObject<RenderBox>(find.byKey(key)).localToGlobal(Offset.zero), Offset.zero);
expect(tester.renderObject<RenderBox>(find.byKey(key)).size, const Size(56.0, 56.0));
});
testWidgets('AppBar positioning of leading and trailing widgets with top padding', (
WidgetTester tester,
) async {
const topPadding100 = MediaQueryData(padding: EdgeInsets.only(top: 100));
final Key leadingKey = UniqueKey();
final Key titleKey = UniqueKey();
final Key trailingKey = UniqueKey();
await tester.pumpWidget(
Localizations(
locale: const Locale('en', 'US'),
delegates: const <LocalizationsDelegate<dynamic>>[
DefaultMaterialLocalizations.delegate,
DefaultWidgetsLocalizations.delegate,
],
child: Directionality(
textDirection: TextDirection.rtl,
child: MediaQuery(
data: topPadding100,
child: Scaffold(
primary: false,
appBar: AppBar(
leading: Placeholder(
key: leadingKey,
), // Forced to 56x56, see _kLeadingWidth in app_bar.dart.
title: Placeholder(key: titleKey, fallbackHeight: kToolbarHeight),
actions: <Widget>[Placeholder(key: trailingKey, fallbackWidth: 10)],
),
),
),
),
),
);
expect(tester.getTopLeft(find.byType(AppBar)), Offset.zero);
expect(tester.getTopLeft(find.byKey(leadingKey)), const Offset(800.0 - 56.0, 100));
expect(tester.getTopLeft(find.byKey(trailingKey)), const Offset(0.0, 100));
// Because the topPadding eliminates the vertical space for the
// NavigationToolbar within the AppBar, the toolbar is constrained
// with minHeight=maxHeight=0. The _AppBarTitle widget vertically centers
// the title, so its Y coordinate relative to the toolbar is -kToolbarHeight / 2
// (-28). The top of the toolbar is at (screen coordinates) y=100, so the
// top of the title is 100 + -28 = 72. The toolbar clips its contents
// so the title isn't actually visible.
expect(
tester.getTopLeft(find.byKey(titleKey)),
const Offset(10 + NavigationToolbar.kMiddleSpacing, 72),
);
});
testWidgets('AppBar excludes header semantics correctly', (WidgetTester tester) async {
final semantics = SemanticsTester(tester);
await tester.pumpWidget(
MaterialApp(
home: Center(
child: AppBar(
leading: const Text('Leading'),
title: const ExcludeSemantics(child: Text('Title')),
excludeHeaderSemantics: true,
actions: const <Widget>[Text('Action 1')],
),
),
),
);
expect(
semantics,
hasSemantics(
TestSemantics.root(
children: <TestSemantics>[
TestSemantics(
children: <TestSemantics>[
TestSemantics(
children: <TestSemantics>[
TestSemantics(
flags: <SemanticsFlag>[SemanticsFlag.scopesRoute],
children: <TestSemantics>[
TestSemantics(
children: <TestSemantics>[
TestSemantics(label: 'Leading', textDirection: TextDirection.ltr),
TestSemantics(label: 'Action 1', textDirection: TextDirection.ltr),
],
),
],
),
],
),
],
),
],
),
ignoreRect: true,
ignoreTransform: true,
ignoreId: true,
),
);
semantics.dispose();
});
testWidgets('AppBar has default semantics order', (WidgetTester tester) async {
final semantics = SemanticsTester(tester);
await tester.pumpWidget(
MaterialApp(
home: Center(
child: AppBar(
leading: Semantics(sortKey: const OrdinalSortKey(0), child: const Text('Leading')),
title: Semantics(sortKey: const OrdinalSortKey(2), child: const Text('Title')),
flexibleSpace: Semantics(
sortKey: const OrdinalSortKey(1),
child: const Text('Flexible Space'),
),
),
),
),
);
expect(
semantics,
hasSemantics(
TestSemantics.root(
children: <TestSemantics>[
TestSemantics(
id: 1,
textDirection: TextDirection.ltr,
children: <TestSemantics>[
TestSemantics(
id: 2,
children: <TestSemantics>[
TestSemantics(
id: 3,
flags: <SemanticsFlag>[SemanticsFlag.scopesRoute],
children: <TestSemantics>[
TestSemantics(
id: 4,
children: <TestSemantics>[
TestSemantics(
id: 7,
children: <TestSemantics>[
TestSemantics(
id: 8,
label: 'Leading',
textDirection: TextDirection.ltr,
),
TestSemantics(
id: 9,
flags: <SemanticsFlag>[
SemanticsFlag.isHeader,
SemanticsFlag.namesRoute,
],
label: 'Title',
textDirection: TextDirection.ltr,
),
],
),
TestSemantics(
id: 5,
children: <TestSemantics>[
TestSemantics(
id: 6,
label: 'Flexible Space',
textDirection: TextDirection.ltr,
),
],
),
],
),
],
),
],
),
],
),
],
),
ignoreRect: true,
ignoreTransform: true,
),
);
semantics.dispose();
});
testWidgets('AppBar can customize sort keys for flexible space', (WidgetTester tester) async {
final semantics = SemanticsTester(tester);
await tester.pumpWidget(
MaterialApp(
home: Center(
child: AppBar(
leading: Semantics(sortKey: const OrdinalSortKey(0), child: const Text('Leading')),
title: Semantics(sortKey: const OrdinalSortKey(2), child: const Text('Title')),
flexibleSpace: Semantics(
sortKey: const OrdinalSortKey(1),
child: const Text('Flexible Space'),
),
useDefaultSemanticsOrder: false,
),
),
),
);
expect(
semantics,
hasSemantics(
TestSemantics.root(
children: <TestSemantics>[
TestSemantics(
id: 1,
textDirection: TextDirection.ltr,
children: <TestSemantics>[
TestSemantics(
id: 2,
children: <TestSemantics>[
TestSemantics(
id: 3,
flags: <SemanticsFlag>[SemanticsFlag.scopesRoute],
children: <TestSemantics>[
TestSemantics(
id: 4,
children: <TestSemantics>[
TestSemantics(
id: 6,
label: 'Leading',
textDirection: TextDirection.ltr,
),
TestSemantics(
id: 5,
label: 'Flexible Space',
textDirection: TextDirection.ltr,
),
TestSemantics(
id: 7,
flags: <SemanticsFlag>[
SemanticsFlag.isHeader,
SemanticsFlag.namesRoute,
],
label: 'Title',
textDirection: TextDirection.ltr,
),
],
),
],
),
],
),
],
),
],
),
ignoreRect: true,
ignoreTransform: true,
),
);
semantics.dispose();
});
// Regression test for https://github.com/flutter/flutter/issues/176566
testWidgets(
'AppBar title Semantics.namesRoute flag should be null on iOS/macOS platforms regardless of theme platform',
(WidgetTester tester) async {
// Regression test for VoiceOver accessibility when theme platform differs from device platform.
// When someone sets theme.platform to TargetPlatform.android on an iOS/macOS device,
// VoiceOver should still work correctly by not having a namesRoute flag in the title's semantics.
final semantics = SemanticsTester(tester);
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(platform: TargetPlatform.android),
home: AppBar(title: const Text('Title')),
),
);
final expectedFlags = <SemanticsFlag>[SemanticsFlag.isHeader];
expect(
semantics,
hasSemantics(
TestSemantics.root(
children: <TestSemantics>[
TestSemantics(
id: 1,
textDirection: TextDirection.ltr,
children: <TestSemantics>[
TestSemantics(
id: 2,
children: <TestSemantics>[
TestSemantics(
id: 3,
flags: <SemanticsFlag>[SemanticsFlag.scopesRoute],
children: <TestSemantics>[
TestSemantics(
id: 4,
children: <TestSemantics>[
TestSemantics(
id: 5,
flags: expectedFlags,
label: 'Title',
textDirection: TextDirection.ltr,
),
],
),
],
),
],
),
],
),
],
),
ignoreRect: true,
ignoreTransform: true,
),
);
semantics.dispose();
},
variant: const TargetPlatformVariant(<TargetPlatform>{
TargetPlatform.iOS,
TargetPlatform.macOS,
}),
);
// Regression test for https://github.com/flutter/flutter/issues/176566
testWidgets(
'AppBar title Semantics.namesRoute flag should be non-null on Android/Fuchsia/Linux/Windows platforms regardless of theme platform',
(WidgetTester tester) async {
// When someone sets theme.platform to TargetPlatform.iOS on an Android device,
// TalkBack should still work correctly by having a namesRoute flag in the title's semantics.
final semantics = SemanticsTester(tester);
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(platform: TargetPlatform.iOS),
home: AppBar(title: const Text('Title')),
),
);
final expectedFlags = <SemanticsFlag>[SemanticsFlag.isHeader, SemanticsFlag.namesRoute];
expect(
semantics,
hasSemantics(
TestSemantics.root(
children: <TestSemantics>[
TestSemantics(
id: 1,
textDirection: TextDirection.ltr,
children: <TestSemantics>[
TestSemantics(
id: 2,
children: <TestSemantics>[
TestSemantics(
id: 3,
flags: <SemanticsFlag>[SemanticsFlag.scopesRoute],
children: <TestSemantics>[
TestSemantics(
id: 4,
children: <TestSemantics>[
TestSemantics(
id: 5,
flags: expectedFlags,
label: 'Title',
textDirection: TextDirection.ltr,
),
],
),
],
),
],
),
],
),
],
),
ignoreRect: true,
ignoreTransform: true,
),
);
semantics.dispose();
},
variant: const TargetPlatformVariant(<TargetPlatform>{
TargetPlatform.android,
TargetPlatform.fuchsia,
TargetPlatform.linux,
TargetPlatform.windows,
}),
);
testWidgets('Material3 - AppBar draws a light system bar for a dark background', (
WidgetTester tester,
) async {
final darkTheme = ThemeData.dark();
await tester.pumpWidget(
MaterialApp(
theme: darkTheme,
home: Scaffold(appBar: AppBar(title: const Text('test'))),
),
);
expect(darkTheme.colorScheme.brightness, Brightness.dark);
expect(
SystemChrome.latestStyle,
const SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
statusBarBrightness: Brightness.dark,
statusBarIconBrightness: Brightness.light,
),
);
});
testWidgets('Material3 - AppBar draws a dark system bar for a light background', (
WidgetTester tester,
) async {
final lightTheme = ThemeData();
await tester.pumpWidget(
MaterialApp(
theme: lightTheme,
home: Scaffold(appBar: AppBar(title: const Text('test'))),
),
);
expect(lightTheme.colorScheme.brightness, Brightness.light);
expect(
SystemChrome.latestStyle,
const SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
statusBarBrightness: Brightness.light,
statusBarIconBrightness: Brightness.dark,
),
);
});
testWidgets(
'Material3 - Default system bar brightness based on AppBar background color brightness.',
(WidgetTester tester) async {
Widget buildAppBar(ThemeData theme) {
return MaterialApp(
theme: theme,
home: Scaffold(appBar: AppBar(title: const Text('Title'))),
);
}
// Using a light theme.
{
await tester.pumpWidget(buildAppBar(ThemeData()));
final Material appBarMaterial = tester.widget<Material>(
find.descendant(of: find.byType(AppBar), matching: find.byType(Material)),
);
final Brightness appBarBrightness = ThemeData.estimateBrightnessForColor(
appBarMaterial.color!,
);
final Brightness onAppBarBrightness = appBarBrightness == Brightness.light
? Brightness.dark
: Brightness.light;
expect(
SystemChrome.latestStyle,
SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
statusBarBrightness: appBarBrightness,
statusBarIconBrightness: onAppBarBrightness,
),
);
}
// Using a dark theme.
{
await tester.pumpWidget(buildAppBar(ThemeData.dark()));
final Material appBarMaterial = tester.widget<Material>(
find.descendant(of: find.byType(AppBar), matching: find.byType(Material)),
);
final Brightness appBarBrightness = ThemeData.estimateBrightnessForColor(
appBarMaterial.color!,
);
final Brightness onAppBarBrightness = appBarBrightness == Brightness.light
? Brightness.dark
: Brightness.light;
expect(
SystemChrome.latestStyle,
SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
statusBarBrightness: appBarBrightness,
statusBarIconBrightness: onAppBarBrightness,
),
);
}
},
);
testWidgets('Material3 - Default status bar color', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
key: GlobalKey(),
theme: ThemeData(appBarTheme: const AppBarThemeData()),
home: Scaffold(appBar: AppBar(title: const Text('title'))),
),
);
expect(SystemChrome.latestStyle!.statusBarColor, Colors.transparent);
});
testWidgets('AppBar systemOverlayStyle is use to style status bar and navigation bar', (
WidgetTester tester,
) async {
final SystemUiOverlayStyle systemOverlayStyle = SystemUiOverlayStyle.light.copyWith(
statusBarColor: Colors.red,
systemNavigationBarColor: Colors.green,
);
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('test'), systemOverlayStyle: systemOverlayStyle),
),
),
);
expect(SystemChrome.latestStyle!.statusBarColor, Colors.red);
expect(SystemChrome.latestStyle!.systemNavigationBarColor, Colors.green);
});
testWidgets('AppBar shape default', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: AppBar(
leading: const Text('L'),
title: const Text('No Scaffold'),
actions: const <Widget>[Text('A1'), Text('A2')],
),
),
);
final Finder appBarFinder = find.byType(AppBar);
AppBar getAppBarWidget(Finder finder) => tester.widget<AppBar>(finder);
expect(getAppBarWidget(appBarFinder).shape, null);
final Finder materialFinder = find.byType(Material);
Material getMaterialWidget(Finder finder) => tester.widget<Material>(finder);
expect(getMaterialWidget(materialFinder).shape, null);
});
testWidgets('AppBar with shape', (WidgetTester tester) async {
const roundedRectangleBorder = RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(15.0)),
);
await tester.pumpWidget(
MaterialApp(
home: AppBar(
leading: const Text('L'),
title: const Text('No Scaffold'),
actions: const <Widget>[Text('A1'), Text('A2')],
shape: roundedRectangleBorder,
),
),
);
final Finder appBarFinder = find.byType(AppBar);
AppBar getAppBarWidget(Finder finder) => tester.widget<AppBar>(finder);
expect(getAppBarWidget(appBarFinder).shape, roundedRectangleBorder);
final Finder materialFinder = find.byType(Material);
Material getMaterialWidget(Finder finder) => tester.widget<Material>(finder);
expect(getMaterialWidget(materialFinder).shape, roundedRectangleBorder);
});
testWidgets('AppBars title has upper limit on text scaling, textScaleFactor = 1, 1.34, 2', (
WidgetTester tester,
) async {
late double textScaleFactor;
Widget buildFrame() {
return MaterialApp(
// Test designed against 2014 font sizes.
theme: ThemeData(textTheme: Typography.englishLike2014),
home: Builder(
builder: (BuildContext context) {
return MediaQuery.withClampedTextScaling(
minScaleFactor: textScaleFactor,
maxScaleFactor: textScaleFactor,
child: Scaffold(
appBar: AppBar(
centerTitle: false,
title: const Text('Jumbo', style: TextStyle(fontSize: 18)),
),
),
);
},
),
);
}
final Finder appBarTitle = find.text('Jumbo');
textScaleFactor = 1;
await tester.pumpWidget(buildFrame());
expect(tester.getRect(appBarTitle).height, 18);
textScaleFactor = 1.34;
await tester.pumpWidget(buildFrame());
expect(tester.getRect(appBarTitle).height, 24);
textScaleFactor = 2;
await tester.pumpWidget(buildFrame());
expect(tester.getRect(appBarTitle).height, 24);
});
testWidgets('AppBars with jumbo titles, textScaleFactor = 3, 3.5, 4', (
WidgetTester tester,
) async {
var textScaleFactor = 1.0;
TextDirection textDirection = TextDirection.ltr;
var centerTitle = false;
Widget buildFrame() {
return MaterialApp(
// Test designed against 2014 font sizes.
theme: ThemeData(textTheme: Typography.englishLike2014),
home: Builder(
builder: (BuildContext context) {
return Directionality(
textDirection: textDirection,
child: Builder(
builder: (BuildContext context) {
return Scaffold(
appBar: AppBar(
centerTitle: centerTitle,
title: MediaQuery.withClampedTextScaling(
minScaleFactor: textScaleFactor,
maxScaleFactor: textScaleFactor,
child: const Text('Jumbo'),
),
),
);
},
),
);
},
),
);
}
final Finder appBarTitle = find.text('Jumbo');
final Finder toolbar = find.byType(NavigationToolbar);
// Overall screen size is 800x600
// Left or right justified title is padded by 16 on the "start" side.
// Toolbar height is 56.
// "Jumbo" title is 100x20.
await tester.pumpWidget(buildFrame());
expect(tester.getRect(appBarTitle), const Rect.fromLTRB(16, 18, 116, 38));
expect(tester.getCenter(appBarTitle).dy, tester.getCenter(toolbar).dy);
textScaleFactor = 3; // "Jumbo" title is 300x60.
await tester.pumpWidget(buildFrame());
expect(tester.getRect(appBarTitle), const Rect.fromLTRB(16, -2, 316, 58));
expect(tester.getCenter(appBarTitle).dy, tester.getCenter(toolbar).dy);
textScaleFactor = 3.5; // "Jumbo" title is 350x70.
await tester.pumpWidget(buildFrame());
expect(tester.getRect(appBarTitle), const Rect.fromLTRB(16, -7, 366, 63));
expect(tester.getCenter(appBarTitle).dy, tester.getCenter(toolbar).dy);
textScaleFactor = 4; // "Jumbo" title is 400x80.
await tester.pumpWidget(buildFrame());
expect(tester.getRect(appBarTitle), const Rect.fromLTRB(16, -12, 416, 68));
expect(tester.getCenter(appBarTitle).dy, tester.getCenter(toolbar).dy);
textDirection = TextDirection.rtl; // Changed to rtl. "Jumbo" title is still 400x80.
await tester.pumpWidget(buildFrame());
expect(
tester.getRect(appBarTitle),
const Rect.fromLTRB(800.0 - 400.0 - 16.0, -12, 800.0 - 16.0, 68),
);
expect(tester.getCenter(appBarTitle).dy, tester.getCenter(toolbar).dy);
centerTitle = true; // Changed to true. "Jumbo" title is still 400x80.
await tester.pumpWidget(buildFrame());
expect(tester.getRect(appBarTitle), const Rect.fromLTRB(200, -12, 800.0 - 200.0, 68));
expect(tester.getCenter(appBarTitle).dy, tester.getCenter(toolbar).dy);
});
testWidgets('AppBar respects toolbarHeight', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('Title'), toolbarHeight: 48),
body: Container(),
),
),
);
expect(appBarHeight(tester), 48);
});
testWidgets('AppBar respects leadingWidth', (WidgetTester tester) async {
const key = Key('leading');
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
appBar: AppBar(
leading: const Placeholder(key: key),
leadingWidth: 100,
title: const Text('Title'),
),
),
),
);
// By default toolbarHeight is 56.0.
expect(tester.getRect(find.byKey(key)), const Rect.fromLTRB(0, 0, 100, 56));
});
testWidgets("AppBar with EndDrawer doesn't have leading", (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(appBar: AppBar(), endDrawer: const Drawer()),
),
);
final Finder endDrawerFinder = find.byTooltip('Open navigation menu');
await tester.tap(endDrawerFinder);
await tester.pump();
final Finder appBarFinder = find.byType(NavigationToolbar);
NavigationToolbar getAppBarWidget(Finder finder) => tester.widget<NavigationToolbar>(finder);
expect(getAppBarWidget(appBarFinder).leading, null);
});
testWidgets('AppBar.titleSpacing defaults to NavigationToolbar.kMiddleSpacing', (
WidgetTester tester,
) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(appBar: AppBar(title: const Text('Title'))),
),
);
final NavigationToolbar navToolBar = tester.widget(find.byType(NavigationToolbar));
expect(navToolBar.middleSpacing, NavigationToolbar.kMiddleSpacing);
});
testWidgets('AppBar foregroundColor and backgroundColor', (WidgetTester tester) async {
const foregroundColor = Color(0xff00ff00);
const backgroundColor = Color(0xff00ffff);
final Key leadingIconKey = UniqueKey();
final Key actionIconKey = UniqueKey();
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
appBar: AppBar(
foregroundColor: foregroundColor,
backgroundColor: backgroundColor,
leading: Icon(Icons.add_circle, key: leadingIconKey),
title: const Text('title'),
actions: <Widget>[
Icon(Icons.ac_unit, key: actionIconKey),
const Text('action'),
],
),
),
),
);
final Material appBarMaterial = tester.widget<Material>(
find.descendant(of: find.byType(AppBar), matching: find.byType(Material)),
);
expect(appBarMaterial.color, backgroundColor);
final TextStyle titleTextStyle = tester
.widget<DefaultTextStyle>(
find.ancestor(of: find.text('title'), matching: find.byType(DefaultTextStyle)).first,
)
.style;
expect(titleTextStyle.color, foregroundColor);
final IconThemeData leadingIconTheme = tester
.widget<IconTheme>(
find.ancestor(of: find.byKey(leadingIconKey), matching: find.byType(IconTheme)).first,
)
.data;
expect(leadingIconTheme.color, foregroundColor);
final IconThemeData actionIconTheme = tester
.widget<IconTheme>(
find.ancestor(of: find.byKey(actionIconKey), matching: find.byType(IconTheme)).first,
)
.data;
expect(actionIconTheme.color, foregroundColor);
// Test icon color
Color? leadingIconColor() => _iconStyle(tester, Icons.add_circle)?.color;
Color? actionIconColor() => _iconStyle(tester, Icons.ac_unit)?.color;
expect(leadingIconColor(), foregroundColor);
expect(actionIconColor(), foregroundColor);
});
testWidgets('Leading, title, and actions show correct default colors', (
WidgetTester tester,
) async {
final themeData = ThemeData.from(
colorScheme: const ColorScheme.light(
onPrimary: Colors.blue,
onSurface: Colors.red,
onSurfaceVariant: Colors.yellow,
),
);
final bool material3 = themeData.useMaterial3;
await tester.pumpWidget(
MaterialApp(
theme: themeData,
home: Scaffold(
appBar: AppBar(
leading: const Icon(Icons.add_circle),
title: const Text('title'),
actions: const <Widget>[Icon(Icons.ac_unit)],
),
),
),
);
Color textColor() {
return tester.renderObject<RenderParagraph>(find.text('title')).text.style!.color!;
}
Color? leadingIconColor() => _iconStyle(tester, Icons.add_circle)?.color;
Color? actionIconColor() => _iconStyle(tester, Icons.ac_unit)?.color;
// M2 default color are onPrimary, and M3 has onSurface for leading and title,
// onSurfaceVariant for actions.
expect(textColor(), material3 ? Colors.red : Colors.blue);
expect(leadingIconColor(), material3 ? Colors.red : Colors.blue);
expect(actionIconColor(), material3 ? Colors.yellow : Colors.blue);
});
// Regression test for https://github.com/flutter/flutter/issues/107305
group('Material3 - Icons are colored correctly by IconTheme and ActionIconTheme', () {
testWidgets('Material3 - Icons and IconButtons are colored by IconTheme', (
WidgetTester tester,
) async {
const iconColor = Color(0xff00ff00);
final Key leadingIconKey = UniqueKey();
final Key actionIconKey = UniqueKey();
await tester.pumpWidget(
MaterialApp(
theme: ThemeData.from(colorScheme: const ColorScheme.light()),
home: Scaffold(
appBar: AppBar(
iconTheme: const IconThemeData(color: iconColor),
leading: Icon(Icons.add_circle, key: leadingIconKey),
title: const Text('title'),
actions: <Widget>[
Icon(Icons.ac_unit, key: actionIconKey),
IconButton(icon: const Icon(Icons.add), onPressed: () {}),
],
),
),
),
);
Color? leadingIconColor() => _iconStyle(tester, Icons.add_circle)?.color;
Color? actionIconColor() => _iconStyle(tester, Icons.ac_unit)?.color;
Color? actionIconButtonColor() => _iconStyle(tester, Icons.add)?.color;
expect(leadingIconColor(), iconColor);
expect(actionIconColor(), iconColor);
expect(actionIconButtonColor(), iconColor);
});
testWidgets('Material3 - Action icons and IconButtons are colored by ActionIconTheme', (
WidgetTester tester,
) async {
final themeData = ThemeData.from(colorScheme: const ColorScheme.light());
const actionsIconColor = Color(0xff0000ff);
final Key leadingIconKey = UniqueKey();
final Key actionIconKey = UniqueKey();
await tester.pumpWidget(
MaterialApp(
theme: themeData,
home: Scaffold(
appBar: AppBar(
actionsIconTheme: const IconThemeData(color: actionsIconColor),
leading: Icon(Icons.add_circle, key: leadingIconKey),
title: const Text('title'),
actions: <Widget>[
Icon(Icons.ac_unit, key: actionIconKey),
IconButton(icon: const Icon(Icons.add), onPressed: () {}),
],
),
),
),
);
Color? leadingIconColor() => _iconStyle(tester, Icons.add_circle)?.color;
Color? actionIconColor() => _iconStyle(tester, Icons.ac_unit)?.color;
Color? actionIconButtonColor() => _iconStyle(tester, Icons.add)?.color;
expect(leadingIconColor(), themeData.colorScheme.onSurface);
expect(actionIconColor(), actionsIconColor);
expect(actionIconButtonColor(), actionsIconColor);
});
testWidgets('Material3 - The actionIconTheme property overrides iconTheme', (
WidgetTester tester,
) async {
final themeData = ThemeData.from(colorScheme: const ColorScheme.light());
const overallIconColor = Color(0xff00ff00);
const actionsIconColor = Color(0xff0000ff);
final Key leadingIconKey = UniqueKey();
final Key actionIconKey = UniqueKey();
await tester.pumpWidget(
MaterialApp(
theme: themeData,
home: Scaffold(
appBar: AppBar(
iconTheme: const IconThemeData(color: overallIconColor),
actionsIconTheme: const IconThemeData(color: actionsIconColor),
leading: Icon(Icons.add_circle, key: leadingIconKey),
title: const Text('title'),
actions: <Widget>[
Icon(Icons.ac_unit, key: actionIconKey),
IconButton(icon: const Icon(Icons.add), onPressed: () {}),
],
),
),
),
);
Color? leadingIconColor() => _iconStyle(tester, Icons.add_circle)?.color;
Color? actionIconColor() => _iconStyle(tester, Icons.ac_unit)?.color;
Color? actionIconButtonColor() => _iconStyle(tester, Icons.add)?.color;
expect(leadingIconColor(), overallIconColor);
expect(actionIconColor(), actionsIconColor);
expect(actionIconButtonColor(), actionsIconColor);
});
testWidgets(
'Material3 - AppBar.iconTheme should override any IconButtonTheme present in the theme',
(WidgetTester tester) async {
final themeData = ThemeData(
iconButtonTheme: IconButtonThemeData(
style: IconButton.styleFrom(foregroundColor: Colors.red, iconSize: 32.0),
),
);
const overallIconTheme = IconThemeData(color: Colors.yellow, size: 30.0);
await tester.pumpWidget(
MaterialApp(
theme: themeData,
home: Scaffold(
appBar: AppBar(
iconTheme: overallIconTheme,
leading: IconButton(icon: const Icon(Icons.menu), onPressed: () {}),
title: const Text('title'),
actions: <Widget>[IconButton(icon: const Icon(Icons.add), onPressed: () {})],
),
),
),
);
Color? leadingIconButtonColor() => _iconStyle(tester, Icons.menu)?.color;
double? leadingIconButtonSize() => _iconStyle(tester, Icons.menu)?.fontSize;
Color? actionIconButtonColor() => _iconStyle(tester, Icons.add)?.color;
double? actionIconButtonSize() => _iconStyle(tester, Icons.menu)?.fontSize;
expect(leadingIconButtonColor(), Colors.yellow);
expect(leadingIconButtonSize(), 30.0);
expect(actionIconButtonColor(), Colors.yellow);
expect(actionIconButtonSize(), 30.0);
},
);
testWidgets(
'Material3 - AppBar.iconTheme should override any IconButtonTheme present in the theme for widgets containing an iconButton',
(WidgetTester tester) async {
final themeData = ThemeData(
iconButtonTheme: IconButtonThemeData(
style: IconButton.styleFrom(foregroundColor: Colors.red, iconSize: 32.0),
),
);
const overallIconTheme = IconThemeData(color: Colors.yellow, size: 30.0);
await tester.pumpWidget(
MaterialApp(
theme: themeData,
home: Scaffold(
appBar: AppBar(
iconTheme: overallIconTheme,
leading: BackButton(onPressed: () {}),
title: const Text('title'),
),
),
),
);
Color? leadingIconButtonColor() => _iconStyle(tester, Icons.arrow_back)?.color;
double? leadingIconButtonSize() => _iconStyle(tester, Icons.arrow_back)?.fontSize;
expect(leadingIconButtonColor(), Colors.yellow);
expect(leadingIconButtonSize(), 30.0);
},
);
testWidgets(
'Material3 - AppBar.actionsIconTheme should override any IconButtonTheme present in the theme',
(WidgetTester tester) async {
final themeData = ThemeData(
iconButtonTheme: IconButtonThemeData(
style: IconButton.styleFrom(foregroundColor: Colors.red, iconSize: 32.0),
),
);
const actionsIconTheme = IconThemeData(color: Colors.yellow, size: 30.0);
await tester.pumpWidget(
MaterialApp(
theme: themeData,
home: Scaffold(
appBar: AppBar(
actionsIconTheme: actionsIconTheme,
title: const Text('title'),
leading: IconButton(icon: const Icon(Icons.menu), onPressed: () {}),
actions: <Widget>[IconButton(icon: const Icon(Icons.add), onPressed: () {})],
),
),
),
);
Color? leadingIconButtonColor() => _iconStyle(tester, Icons.menu)?.color;
double? leadingIconButtonSize() => _iconStyle(tester, Icons.menu)?.fontSize;
Color? actionIconButtonColor() => _iconStyle(tester, Icons.add)?.color;
double? actionIconButtonSize() => _iconStyle(tester, Icons.add)?.fontSize;
// The leading icon button uses the style in the IconButtonTheme because only actionsIconTheme is provided.
expect(leadingIconButtonColor(), Colors.red);
expect(leadingIconButtonSize(), 32.0);
expect(actionIconButtonColor(), Colors.yellow);
expect(actionIconButtonSize(), 30.0);
},
);
testWidgets(
'Material3 - AppBar.actionsIconTheme should override any IconButtonTheme present in the theme for widgets containing an iconButton',
(WidgetTester tester) async {
final themeData = ThemeData(
iconButtonTheme: IconButtonThemeData(
style: IconButton.styleFrom(foregroundColor: Colors.red, iconSize: 32.0),
),
);
const actionsIconTheme = IconThemeData(color: Colors.yellow, size: 30.0);
await tester.pumpWidget(
MaterialApp(
theme: themeData,
home: Scaffold(
appBar: AppBar(
actionsIconTheme: actionsIconTheme,
title: const Text('title'),
actions: <Widget>[BackButton(onPressed: () {})],
),
),
),
);
Color? actionIconButtonColor() => _iconStyle(tester, Icons.arrow_back)?.color;
double? actionIconButtonSize() => _iconStyle(tester, Icons.arrow_back)?.fontSize;
expect(actionIconButtonColor(), Colors.yellow);
expect(actionIconButtonSize(), 30.0);
},
);
testWidgets(
'Material3 - The foregroundColor property of the AppBar overrides any IconButtonTheme present in the theme',
(WidgetTester tester) async {
final themeData = ThemeData(
iconButtonTheme: IconButtonThemeData(
style: IconButton.styleFrom(foregroundColor: Colors.red),
),
);
await tester.pumpWidget(
MaterialApp(
theme: themeData,
home: Scaffold(
appBar: AppBar(
foregroundColor: Colors.purple,
title: const Text('title'),
leading: IconButton(icon: const Icon(Icons.menu), onPressed: () {}),
actions: <Widget>[IconButton(icon: const Icon(Icons.add), onPressed: () {})],
),
),
),
);
Color? leadingIconButtonColor() => _iconStyle(tester, Icons.menu)?.color;
Color? actionIconButtonColor() => _iconStyle(tester, Icons.add)?.color;
expect(leadingIconButtonColor(), Colors.purple);
expect(actionIconButtonColor(), Colors.purple);
},
);
// This is a regression test for https://github.com/flutter/flutter/issues/130485.
testWidgets('Material3 - AppBar.iconTheme is correctly applied in dark mode', (
WidgetTester tester,
) async {
final themeData = ThemeData(
colorScheme: const ColorScheme.dark().copyWith(onSurfaceVariant: Colors.red),
);
await tester.pumpWidget(
MaterialApp(
theme: themeData,
home: Scaffold(
appBar: AppBar(
iconTheme: const IconThemeData(color: Colors.white),
leading: IconButton(icon: const Icon(Icons.menu), onPressed: () {}),
actions: <Widget>[IconButton(icon: const Icon(Icons.add), onPressed: () {})],
),
),
),
);
Color? leadingIconButtonColor() => _iconStyle(tester, Icons.menu)?.color;
Color? actionIconButtonColor() => _iconStyle(tester, Icons.add)?.color;
expect(leadingIconButtonColor(), Colors.white);
expect(actionIconButtonColor(), Colors.white);
});
// This is a regression test for https://github.com/flutter/flutter/issues/130485.
testWidgets('Material3 - AppBar.foregroundColor is correctly applied in dark mode', (
WidgetTester tester,
) async {
final themeData = ThemeData(
colorScheme: const ColorScheme.dark().copyWith(onSurfaceVariant: Colors.red),
);
await tester.pumpWidget(
MaterialApp(
theme: themeData,
home: Scaffold(
appBar: AppBar(
foregroundColor: Colors.white,
leading: IconButton(icon: const Icon(Icons.menu), onPressed: () {}),
actions: <Widget>[IconButton(icon: const Icon(Icons.add), onPressed: () {})],
),
),
),
);
Color? leadingIconButtonColor() => _iconStyle(tester, Icons.menu)?.color;
Color? actionIconButtonColor() => _iconStyle(tester, Icons.add)?.color;
expect(leadingIconButtonColor(), Colors.white);
expect(actionIconButtonColor(), Colors.white);
});
// This is a regression test for https://github.com/flutter/flutter/issues/130485.
testWidgets('Material3 - AppBar.iconTheme is correctly applied in light mode', (
WidgetTester tester,
) async {
final themeData = ThemeData(
colorScheme: const ColorScheme.light().copyWith(onSurfaceVariant: Colors.red),
);
await tester.pumpWidget(
MaterialApp(
theme: themeData,
home: Scaffold(
appBar: AppBar(
iconTheme: const IconThemeData(color: Colors.black87),
leading: IconButton(icon: const Icon(Icons.menu), onPressed: () {}),
actions: <Widget>[IconButton(icon: const Icon(Icons.add), onPressed: () {})],
),
),
),
);
Color? leadingIconButtonColor() => _iconStyle(tester, Icons.menu)?.color;
Color? actionIconButtonColor() => _iconStyle(tester, Icons.add)?.color;
expect(leadingIconButtonColor(), Colors.black87);
expect(actionIconButtonColor(), Colors.black87);
});
// This is a regression test for https://github.com/flutter/flutter/issues/130485.
testWidgets('Material3 - AppBar.foregroundColor is correctly applied in light mode', (
WidgetTester tester,
) async {
final themeData = ThemeData(
colorScheme: const ColorScheme.light().copyWith(onSurfaceVariant: Colors.red),
);
await tester.pumpWidget(
MaterialApp(
theme: themeData,
home: Scaffold(
appBar: AppBar(
foregroundColor: Colors.black87,
leading: IconButton(icon: const Icon(Icons.menu), onPressed: () {}),
actions: <Widget>[IconButton(icon: const Icon(Icons.add), onPressed: () {})],
),
),
),
);
Color? leadingIconButtonColor() => _iconStyle(tester, Icons.menu)?.color;
Color? actionIconButtonColor() => _iconStyle(tester, Icons.add)?.color;
expect(leadingIconButtonColor(), Colors.black87);
expect(actionIconButtonColor(), Colors.black87);
});
});
group('WidgetStateColor scrolledUnder', () {
const scrolledColor = Color(0xff00ff00);
const defaultColor = Color(0xff0000ff);
Widget buildAppBar({
required double contentHeight,
bool reverse = false,
bool includeFlexibleSpace = false,
bool animateColor = false,
double? scrolledUnderElevation,
}) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
elevation: 0,
scrolledUnderElevation: scrolledUnderElevation,
backgroundColor: WidgetStateColor.resolveWith((Set<WidgetState> states) {
return states.contains(WidgetState.scrolledUnder) ? scrolledColor : defaultColor;
}),
title: const Text('AppBar'),
flexibleSpace: includeFlexibleSpace
? const FlexibleSpaceBar(title: Text('FlexibleSpace'))
: null,
animateColor: animateColor,
),
body: ListView(
reverse: reverse,
children: <Widget>[Container(height: contentHeight, color: Colors.teal)],
),
),
);
}
testWidgets('backgroundColor for horizontal scrolling', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
appBar: AppBar(
elevation: 0,
backgroundColor: WidgetStateColor.resolveWith((Set<WidgetState> states) {
return states.contains(WidgetState.scrolledUnder) ? scrolledColor : defaultColor;
}),
title: const Text('AppBar'),
notificationPredicate: (ScrollNotification notification) {
// Represents both scroll views below being treated as a
// single viewport.
return notification.depth <= 1;
},
),
body: SingleChildScrollView(
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Container(height: 1200, width: 1200, color: Colors.teal),
),
),
),
),
);
expect(getAppBarBackgroundColor(tester), defaultColor);
expect(tester.getSize(findAppBarMaterial()).height, kToolbarHeight);
TestGesture gesture = await tester.startGesture(const Offset(50.0, 400.0));
await gesture.moveBy(const Offset(0.0, -kToolbarHeight));
await tester.pump();
await gesture.moveBy(const Offset(0.0, -kToolbarHeight));
await gesture.up();
await tester.pumpAndSettle();
expect(getAppBarBackgroundColor(tester), scrolledColor);
expect(tester.getSize(findAppBarMaterial()).height, kToolbarHeight);
gesture = await tester.startGesture(const Offset(50.0, 300.0));
// Scroll horizontally
await gesture.moveBy(const Offset(-kToolbarHeight, 0.0));
await tester.pump();
await gesture.moveBy(const Offset(-kToolbarHeight, 0.0));
await gesture.up();
await tester.pumpAndSettle();
// The app bar is still scrolled under vertically, so it should not have
// changed back in response to horizontal scrolling.
expect(getAppBarBackgroundColor(tester), scrolledColor);
expect(tester.getSize(findAppBarMaterial()).height, kToolbarHeight);
});
testWidgets('backgroundColor', (WidgetTester tester) async {
await tester.pumpWidget(buildAppBar(contentHeight: 1200.0));
expect(getAppBarBackgroundColor(tester), defaultColor);
expect(tester.getSize(findAppBarMaterial()).height, kToolbarHeight);
TestGesture gesture = await tester.startGesture(const Offset(50.0, 400.0));
await gesture.moveBy(const Offset(0.0, -kToolbarHeight));
await gesture.up();
await tester.pumpAndSettle();
expect(getAppBarBackgroundColor(tester), scrolledColor);
expect(tester.getSize(findAppBarMaterial()).height, kToolbarHeight);
gesture = await tester.startGesture(const Offset(50.0, 300.0));
await gesture.moveBy(const Offset(0.0, kToolbarHeight));
await gesture.up();
await tester.pumpAndSettle();
expect(getAppBarBackgroundColor(tester), defaultColor);
expect(tester.getSize(findAppBarMaterial()).height, kToolbarHeight);
});
testWidgets('backgroundColor animation', (WidgetTester tester) async {
await tester.pumpWidget(
buildAppBar(contentHeight: 1200.0, scrolledUnderElevation: 0, animateColor: true),
);
expect(getAppBarAnimatedBackgroundColor(tester), defaultColor);
TestGesture gesture = await tester.startGesture(const Offset(50.0, 400.0));
await gesture.moveBy(const Offset(0.0, -kToolbarHeight));
await gesture.up();
await tester.pump();
expect(getAppBarAnimatedBackgroundColor(tester), defaultColor);
await tester.pumpAndSettle();
expect(getAppBarAnimatedBackgroundColor(tester), scrolledColor);
gesture = await tester.startGesture(const Offset(50.0, 300.0));
await gesture.moveBy(const Offset(0.0, kToolbarHeight));
await gesture.up();
await tester.pump();
expect(getAppBarAnimatedBackgroundColor(tester), scrolledColor);
// Check intermediate color values.
await tester.pump(const Duration(milliseconds: 50));
expect(getAppBarAnimatedBackgroundColor(tester), isSameColorAs(const Color(0xFF00C33C)));
await tester.pump(const Duration(milliseconds: 50));
expect(getAppBarAnimatedBackgroundColor(tester), isSameColorAs(const Color(0xFF0039C6)));
await tester.pumpAndSettle();
expect(getAppBarAnimatedBackgroundColor(tester), defaultColor);
});
testWidgets('backgroundColor with FlexibleSpace', (WidgetTester tester) async {
await tester.pumpWidget(buildAppBar(contentHeight: 1200.0, includeFlexibleSpace: true));
expect(getAppBarBackgroundColor(tester), defaultColor);
expect(tester.getSize(findAppBarMaterial()).height, kToolbarHeight);
TestGesture gesture = await tester.startGesture(const Offset(50.0, 400.0));
await gesture.moveBy(const Offset(0.0, -kToolbarHeight));
await gesture.up();
await tester.pumpAndSettle();
expect(getAppBarBackgroundColor(tester), scrolledColor);
expect(tester.getSize(findAppBarMaterial()).height, kToolbarHeight);
gesture = await tester.startGesture(const Offset(50.0, 300.0));
await gesture.moveBy(const Offset(0.0, kToolbarHeight));
await gesture.up();
await tester.pumpAndSettle();
expect(getAppBarBackgroundColor(tester), defaultColor);
expect(tester.getSize(findAppBarMaterial()).height, kToolbarHeight);
});
testWidgets('backgroundColor - reverse', (WidgetTester tester) async {
await tester.pumpWidget(buildAppBar(contentHeight: 1200.0, reverse: true));
await tester.pump();
// In this test case, the content always extends under the AppBar, so it
// should always be the scrolledColor.
expect(getAppBarBackgroundColor(tester), scrolledColor);
expect(tester.getSize(findAppBarMaterial()).height, kToolbarHeight);
TestGesture gesture = await tester.startGesture(const Offset(50.0, 400.0));
await gesture.moveBy(const Offset(0.0, kToolbarHeight));
await gesture.up();
await tester.pumpAndSettle();
expect(getAppBarBackgroundColor(tester), scrolledColor);
expect(tester.getSize(findAppBarMaterial()).height, kToolbarHeight);
gesture = await tester.startGesture(const Offset(50.0, 300.0));
await gesture.moveBy(const Offset(0.0, -kToolbarHeight));
await gesture.up();
await tester.pumpAndSettle();
expect(getAppBarBackgroundColor(tester), scrolledColor);
expect(tester.getSize(findAppBarMaterial()).height, kToolbarHeight);
});
testWidgets('backgroundColor with FlexibleSpace - reverse', (WidgetTester tester) async {
await tester.pumpWidget(
buildAppBar(contentHeight: 1200.0, reverse: true, includeFlexibleSpace: true),
);
await tester.pump();
// In this test case, the content always extends under the AppBar, so it
// should always be the scrolledColor.
expect(getAppBarBackgroundColor(tester), scrolledColor);
expect(tester.getSize(findAppBarMaterial()).height, kToolbarHeight);
TestGesture gesture = await tester.startGesture(const Offset(50.0, 400.0));
await gesture.moveBy(const Offset(0.0, kToolbarHeight));
await gesture.up();
await tester.pumpAndSettle();
expect(getAppBarBackgroundColor(tester), scrolledColor);
expect(tester.getSize(findAppBarMaterial()).height, kToolbarHeight);
gesture = await tester.startGesture(const Offset(50.0, 300.0));
await gesture.moveBy(const Offset(0.0, -kToolbarHeight));
await gesture.up();
await tester.pumpAndSettle();
expect(getAppBarBackgroundColor(tester), scrolledColor);
expect(tester.getSize(findAppBarMaterial()).height, kToolbarHeight);
});
testWidgets('_handleScrollNotification safely calls setState()', (WidgetTester tester) async {
// Regression test for failures found in Google internal issue b/185192049.
final controller = ScrollController(initialScrollOffset: 400);
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('AppBar')),
body: Scrollbar(
thumbVisibility: true,
controller: controller,
child: ListView(
controller: controller,
children: <Widget>[Container(height: 1200.0, color: Colors.teal)],
),
),
),
),
);
expect(tester.takeException(), isNull);
controller.dispose();
});
testWidgets('does not trigger on horizontal scroll', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
appBar: AppBar(
elevation: 0,
backgroundColor: WidgetStateColor.resolveWith((Set<WidgetState> states) {
return states.contains(WidgetState.scrolledUnder) ? scrolledColor : defaultColor;
}),
title: const Text('AppBar'),
),
body: ListView(
scrollDirection: Axis.horizontal,
children: <Widget>[Container(height: 600.0, width: 1200.0, color: Colors.teal)],
),
),
),
);
expect(getAppBarBackgroundColor(tester), defaultColor);
TestGesture gesture = await tester.startGesture(const Offset(50.0, 400.0));
await gesture.moveBy(const Offset(-100.0, 0.0));
await gesture.up();
await tester.pumpAndSettle();
expect(getAppBarBackgroundColor(tester), defaultColor);
gesture = await tester.startGesture(const Offset(50.0, 400.0));
await gesture.moveBy(const Offset(100.0, 0.0));
await gesture.up();
await tester.pumpAndSettle();
expect(getAppBarBackgroundColor(tester), defaultColor);
});
testWidgets('backgroundColor - not triggered in reverse for short content', (
WidgetTester tester,
) async {
await tester.pumpWidget(buildAppBar(contentHeight: 200.0, reverse: true));
await tester.pump();
// In reverse, the content here is not long enough to scroll under the app
// bar.
expect(getAppBarBackgroundColor(tester), defaultColor);
expect(tester.getSize(findAppBarMaterial()).height, kToolbarHeight);
final TestGesture gesture = await tester.startGesture(const Offset(50.0, 400.0));
await gesture.moveBy(const Offset(0.0, kToolbarHeight));
await gesture.up();
await tester.pumpAndSettle();
expect(getAppBarBackgroundColor(tester), defaultColor);
expect(tester.getSize(findAppBarMaterial()).height, kToolbarHeight);
});
testWidgets('backgroundColor with FlexibleSpace - not triggered in reverse for short content', (
WidgetTester tester,
) async {
await tester.pumpWidget(
buildAppBar(contentHeight: 200.0, reverse: true, includeFlexibleSpace: true),
);
await tester.pump();
// In reverse, the content here is not long enough to scroll under the app
// bar.
expect(getAppBarBackgroundColor(tester), defaultColor);
expect(tester.getSize(findAppBarMaterial()).height, kToolbarHeight);
final TestGesture gesture = await tester.startGesture(const Offset(50.0, 400.0));
await gesture.moveBy(const Offset(0.0, kToolbarHeight));
await gesture.up();
await tester.pumpAndSettle();
expect(getAppBarBackgroundColor(tester), defaultColor);
expect(tester.getSize(findAppBarMaterial()).height, kToolbarHeight);
});
testWidgets('scrolledUnderElevation should be maintained when drawer is opened', (
WidgetTester tester,
) async {
final GlobalKey drawerListKey = GlobalKey();
final GlobalKey bodyListKey = GlobalKey();
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
appBar: AppBar(
elevation: 0,
backgroundColor: WidgetStateColor.resolveWith((Set<WidgetState> states) {
return states.contains(WidgetState.scrolledUnder) ? scrolledColor : defaultColor;
}),
title: const Text('AppBar'),
),
drawer: Drawer(
child: ListView(
key: drawerListKey,
children: <Widget>[Container(height: 1200, color: Colors.red)],
),
),
body: ListView(
key: bodyListKey,
children: <Widget>[Container(height: 1200, color: Colors.teal)],
),
),
),
);
// Initial state: AppBar should have the default color.
expect(getAppBarBackgroundColor(tester), defaultColor);
// Scroll the list view.
await tester.drag(find.byKey(bodyListKey), const Offset(0, -300));
await tester.pumpAndSettle();
// The AppBar should now have the scrolled color.
expect(getAppBarBackgroundColor(tester), scrolledColor);
// Open the drawer.
await tester.tap(find.byIcon(Icons.menu));
await tester.pumpAndSettle();
// The AppBar should still have the scrolled color.
expect(getAppBarBackgroundColor(tester), scrolledColor);
// Scroll the list inside the drawer.
await tester.drag(find.byKey(drawerListKey), const Offset(0, -300));
await tester.pumpAndSettle();
// The AppBar should still have the scrolled color.
expect(getAppBarBackgroundColor(tester), scrolledColor);
// Scroll list inside the drawer back to the top.
await tester.drag(find.byKey(drawerListKey), const Offset(0, 300));
await tester.pumpAndSettle();
// The AppBar should still have the scrolled color.
expect(getAppBarBackgroundColor(tester), scrolledColor);
// Close the drawer using the Scaffold's method.
tester.state<ScaffoldState>(find.byType(Scaffold)).closeDrawer();
await tester.pumpAndSettle();
// The AppBar should still have the scrolled color.
expect(getAppBarBackgroundColor(tester), scrolledColor);
// Scroll the list view back to the top.
await tester.drag(find.byKey(bodyListKey), const Offset(0, 300));
await tester.pumpAndSettle();
// The AppBar should be back to the default color.
expect(getAppBarBackgroundColor(tester), defaultColor);
});
testWidgets('scrolledUnderElevation should be maintained when endDrawer is opened', (
WidgetTester tester,
) async {
final GlobalKey drawerListKey = GlobalKey();
final GlobalKey bodyListKey = GlobalKey();
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
appBar: AppBar(
elevation: 0,
backgroundColor: WidgetStateColor.resolveWith((Set<WidgetState> states) {
return states.contains(WidgetState.scrolledUnder) ? scrolledColor : defaultColor;
}),
title: const Text('AppBar'),
),
endDrawer: Drawer(
child: ListView(
key: drawerListKey,
children: <Widget>[Container(height: 1200, color: Colors.red)],
),
),
body: ListView(
key: bodyListKey,
children: <Widget>[Container(height: 1200, color: Colors.teal)],
),
),
),
);
// Initial state: AppBar should have the default color.
expect(getAppBarBackgroundColor(tester), defaultColor);
// Scroll the list view.
await tester.drag(find.byKey(bodyListKey), const Offset(0, -300));
await tester.pumpAndSettle();
// The AppBar should now have the scrolled color.
expect(getAppBarBackgroundColor(tester), scrolledColor);
// Open the drawer.
await tester.tap(find.byIcon(Icons.menu));
await tester.pumpAndSettle();
// The AppBar should still have the scrolled color.
expect(getAppBarBackgroundColor(tester), scrolledColor);
// Scroll the list inside the drawer.
await tester.drag(find.byKey(drawerListKey), const Offset(0, -300));
await tester.pumpAndSettle();
// The AppBar should still have the scrolled color.
expect(getAppBarBackgroundColor(tester), scrolledColor);
// Scroll list inside the drawer back to the top.
await tester.drag(find.byKey(drawerListKey), const Offset(0, 300));
await tester.pumpAndSettle();
// The AppBar should still have the scrolled color.
expect(getAppBarBackgroundColor(tester), scrolledColor);
// Close the drawer using the Scaffold's method.
tester.state<ScaffoldState>(find.byType(Scaffold)).closeEndDrawer();
await tester.pumpAndSettle();
// The AppBar should still have the scrolled color.
expect(getAppBarBackgroundColor(tester), scrolledColor);
// Scroll the list view back to the top.
await tester.drag(find.byKey(bodyListKey), const Offset(0, 300));
await tester.pumpAndSettle();
// The AppBar should be back to the default color.
expect(getAppBarBackgroundColor(tester), defaultColor);
});
});
// Regression test for https://github.com/flutter/flutter/issues/80256
testWidgets('The second page should have a back button even it has an end drawer', (
WidgetTester tester,
) async {
final Page<void> page1 = MaterialPage<void>(
key: const ValueKey<String>('1'),
child: Scaffold(
key: const ValueKey<String>('1'),
appBar: AppBar(),
endDrawer: const Drawer(),
),
);
final Page<void> page2 = MaterialPage<void>(
key: const ValueKey<String>('2'),
child: Scaffold(
key: const ValueKey<String>('2'),
appBar: AppBar(),
endDrawer: const Drawer(),
),
);
final pages = <Page<void>>[page1, page2];
await tester.pumpWidget(
MaterialApp(
home: Navigator(pages: pages, onPopPage: (Route<Object?> route, Object? result) => false),
),
);
// The page2 should have a back button.
expect(
find.descendant(
of: find.byKey(const ValueKey<String>('2')),
matching: find.byType(BackButton),
),
findsOneWidget,
);
});
testWidgets('Only local entries that imply app bar dismissal will introduce an back button', (
WidgetTester tester,
) async {
final GlobalKey key = GlobalKey();
await tester.pumpWidget(
MaterialApp(
home: Scaffold(key: key, appBar: AppBar()),
),
);
expect(find.byType(BackButton), findsNothing);
// Push one entry that doesn't imply app bar dismissal.
ModalRoute.of(
key.currentContext!,
)!.addLocalHistoryEntry(LocalHistoryEntry(onRemove: () {}, impliesAppBarDismissal: false));
await tester.pump();
expect(find.byType(BackButton), findsNothing);
// Push one entry that implies app bar dismissal.
ModalRoute.of(key.currentContext!)!.addLocalHistoryEntry(LocalHistoryEntry(onRemove: () {}));
await tester.pump();
expect(find.byType(BackButton), findsOneWidget);
});
testWidgets('AppBar.preferredHeightFor', (WidgetTester tester) async {
late double preferredHeight;
late Size preferredSize;
Widget buildFrame({double? themeToolbarHeight, double? appBarToolbarHeight}) {
final appBar = AppBar(toolbarHeight: appBarToolbarHeight);
return MaterialApp(
theme: ThemeData(appBarTheme: AppBarThemeData(toolbarHeight: themeToolbarHeight)),
home: Builder(
builder: (BuildContext context) {
preferredHeight = AppBar.preferredHeightFor(context, appBar.preferredSize);
preferredSize = appBar.preferredSize;
return Scaffold(appBar: appBar, body: const Placeholder());
},
),
);
}
await tester.pumpWidget(buildFrame());
expect(tester.getSize(find.byType(AppBar)).height, kToolbarHeight);
expect(preferredHeight, kToolbarHeight);
expect(preferredSize.height, kToolbarHeight);
await tester.pumpWidget(buildFrame(themeToolbarHeight: 96));
await tester.pumpAndSettle(); // Animate MaterialApp theme change.
expect(tester.getSize(find.byType(AppBar)).height, 96);
expect(preferredHeight, 96);
// Special case: AppBarTheme.toolbarHeight specified,
// AppBar.theme.toolbarHeight is null.
expect(preferredSize.height, kToolbarHeight);
await tester.pumpWidget(buildFrame(appBarToolbarHeight: 64));
await tester.pumpAndSettle(); // Animate MaterialApp theme change.
expect(tester.getSize(find.byType(AppBar)).height, 64);
expect(preferredHeight, 64);
expect(preferredSize.height, 64);
await tester.pumpWidget(buildFrame(appBarToolbarHeight: 64, themeToolbarHeight: 96));
await tester.pumpAndSettle(); // Animate MaterialApp theme change.
expect(tester.getSize(find.byType(AppBar)).height, 64);
expect(preferredHeight, 64);
expect(preferredSize.height, 64);
});
testWidgets('AppBar title with actions should have the same position regardless of centerTitle', (
WidgetTester tester,
) async {
final Key titleKey = UniqueKey();
var centerTitle = false;
Widget buildApp() {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
centerTitle: centerTitle,
title: Container(
key: titleKey,
constraints: BoxConstraints.loose(const Size(1000.0, 1000.0)),
),
actions: const <Widget>[SizedBox(width: 48.0)],
),
),
);
}
await tester.pumpWidget(buildApp());
final Finder title = find.byKey(titleKey);
expect(tester.getTopLeft(title).dx, 16.0);
centerTitle = true;
await tester.pumpWidget(buildApp());
expect(tester.getTopLeft(title).dx, 16.0);
});
testWidgets('AppBar leading widget can take up arbitrary space', (WidgetTester tester) async {
final Key leadingKey = UniqueKey();
final Key titleKey = UniqueKey();
late double leadingWidth;
Widget buildApp() {
return MaterialApp(
home: LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
leadingWidth = constraints.maxWidth / 2;
return Scaffold(
appBar: AppBar(
leading: Container(key: leadingKey, width: leadingWidth),
leadingWidth: leadingWidth,
title: Text('Title', key: titleKey),
),
);
},
),
);
}
await tester.pumpWidget(buildApp());
expect(tester.getTopLeft(find.byKey(titleKey)).dx, leadingWidth + 16.0);
expect(tester.getSize(find.byKey(leadingKey)).width, leadingWidth);
});
group('AppBar.forceMaterialTransparency', () {
Material getAppBarMaterial(WidgetTester tester) {
return tester.widget<Material>(
find.descendant(of: find.byType(AppBar), matching: find.byType(Material)).first,
);
}
// Generates a MaterialApp with an AppBar with a TextButton beneath it
// (via extendBodyBehindAppBar = true).
Widget buildWidget({required bool forceMaterialTransparency, required VoidCallback onPressed}) {
return MaterialApp(
home: Scaffold(
extendBodyBehindAppBar: true,
appBar: AppBar(
forceMaterialTransparency: forceMaterialTransparency,
elevation: 3,
backgroundColor: Colors.red,
title: const Text('AppBar'),
),
body: Align(
alignment: Alignment.topCenter,
child: TextButton(onPressed: onPressed, child: const Text('press me')),
),
),
);
}
testWidgets('forceMaterialTransparency == true allows gestures beneath the app bar', (
WidgetTester tester,
) async {
var buttonWasPressed = false;
final Widget widget = buildWidget(
forceMaterialTransparency: true,
onPressed: () {
buttonWasPressed = true;
},
);
await tester.pumpWidget(widget);
final Material material = getAppBarMaterial(tester);
expect(material.type, MaterialType.transparency);
final Finder buttonFinder = find.byType(TextButton);
await tester.tap(buttonFinder);
await tester.pump();
expect(buttonWasPressed, isTrue);
});
testWidgets('forceMaterialTransparency == false does not allow gestures beneath the app bar', (
WidgetTester tester,
) async {
// Set this, and tester.tap(warnIfMissed:false), to suppress
// errors/warning that the button is not hittable (which is expected).
WidgetController.hitTestWarningShouldBeFatal = false;
var buttonWasPressed = false;
final Widget widget = buildWidget(
forceMaterialTransparency: false,
onPressed: () {
buttonWasPressed = true;
},
);
await tester.pumpWidget(widget);
final Material material = getAppBarMaterial(tester);
expect(material.type, MaterialType.canvas);
final Finder buttonFinder = find.byType(TextButton);
await tester.tap(buttonFinder, warnIfMissed: false);
await tester.pump();
expect(buttonWasPressed, isFalse);
});
});
testWidgets('AppBar.leading size with custom IconButton', (WidgetTester tester) async {
final Key leadingKey = UniqueKey();
final Key titleKey = UniqueKey();
const titleSpacing = 16.0;
final theme = ThemeData();
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
appBar: AppBar(
leading: IconButton(key: leadingKey, onPressed: () {}, icon: const Icon(Icons.menu)),
centerTitle: false,
title: Text('Title', key: titleKey),
),
),
),
);
final Finder buttonFinder = find.byType(IconButton);
expect(tester.getSize(buttonFinder), const Size(48.0, 48.0));
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer();
await gesture.moveTo(tester.getCenter(buttonFinder));
await tester.pumpAndSettle();
expect(
buttonFinder,
paints..rect(
rect: const Rect.fromLTRB(0.0, 0.0, 40.0, 40.0),
color: theme.colorScheme.onSurface.withOpacity(0.08),
),
);
// Get the offset of the Center widget that wraps the IconButton.
final Offset backButtonOffset = tester.getTopRight(
find.ancestor(of: buttonFinder, matching: find.byType(Center)),
);
final Offset titleOffset = tester.getTopLeft(find.byKey(titleKey));
expect(titleOffset.dx, backButtonOffset.dx + titleSpacing);
});
testWidgets('AppBar.leading size with custom BackButton', (WidgetTester tester) async {
final Key leadingKey = UniqueKey();
final Key titleKey = UniqueKey();
const titleSpacing = 16.0;
final theme = ThemeData();
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
appBar: AppBar(
leading: BackButton(key: leadingKey, onPressed: () {}),
centerTitle: false,
title: Text('Title', key: titleKey),
),
),
),
);
final Finder buttonFinder = find.byType(BackButton);
expect(tester.getSize(buttonFinder), const Size(48.0, 48.0));
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer();
await gesture.moveTo(tester.getCenter(buttonFinder));
await tester.pumpAndSettle();
expect(
buttonFinder,
paints..rect(
rect: const Rect.fromLTRB(0.0, 0.0, 40.0, 40.0),
color: theme.colorScheme.onSurface.withOpacity(0.08),
),
);
// Get the offset of the Center widget that wraps the IconButton.
final Offset backButtonOffset = tester.getTopRight(
find.ancestor(of: buttonFinder, matching: find.byType(Center)),
);
final Offset titleOffset = tester.getTopLeft(find.byKey(titleKey));
expect(titleOffset.dx, backButtonOffset.dx + titleSpacing);
});
testWidgets('AppBar.leading size with custom CloseButton', (WidgetTester tester) async {
final Key leadingKey = UniqueKey();
final Key titleKey = UniqueKey();
const titleSpacing = 16.0;
final theme = ThemeData();
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
appBar: AppBar(
leading: CloseButton(key: leadingKey, onPressed: () {}),
centerTitle: false,
title: Text('Title', key: titleKey),
),
),
),
);
final Finder buttonFinder = find.byType(CloseButton);
expect(tester.getSize(buttonFinder), const Size(48.0, 48.0));
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer();
await gesture.moveTo(tester.getCenter(buttonFinder));
await tester.pumpAndSettle();
expect(
buttonFinder,
paints..rect(
rect: const Rect.fromLTRB(0.0, 0.0, 40.0, 40.0),
color: theme.colorScheme.onSurface.withOpacity(0.08),
),
);
// Get the offset of the Center widget that wraps the IconButton.
final Offset backButtonOffset = tester.getTopRight(
find.ancestor(of: buttonFinder, matching: find.byType(Center)),
);
final Offset titleOffset = tester.getTopLeft(find.byKey(titleKey));
expect(titleOffset.dx, backButtonOffset.dx + titleSpacing);
});
testWidgets('AppBar.leading size with custom DrawerButton', (WidgetTester tester) async {
final Key leadingKey = UniqueKey();
final Key titleKey = UniqueKey();
const titleSpacing = 16.0;
final theme = ThemeData();
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
appBar: AppBar(
leading: DrawerButton(key: leadingKey, onPressed: () {}),
centerTitle: false,
title: Text('Title', key: titleKey),
),
),
),
);
final Finder buttonFinder = find.byType(DrawerButton);
expect(tester.getSize(buttonFinder), const Size(48.0, 48.0));
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer();
await gesture.moveTo(tester.getCenter(buttonFinder));
await tester.pumpAndSettle();
expect(
buttonFinder,
paints..rect(
rect: const Rect.fromLTRB(0.0, 0.0, 40.0, 40.0),
color: theme.colorScheme.onSurface.withOpacity(0.08),
),
);
// Get the offset of the Center widget that wraps the IconButton.
final Offset backButtonOffset = tester.getTopRight(
find.ancestor(of: buttonFinder, matching: find.byType(Center)),
);
final Offset titleOffset = tester.getTopLeft(find.byKey(titleKey));
expect(titleOffset.dx, backButtonOffset.dx + titleSpacing);
});
// Regression test for https://github.com/flutter/flutter/issues/152315
testWidgets('AppBar back button navigates to previous page on tap with TooltipTriggerMode.tap', (
WidgetTester tester,
) async {
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(tooltipTheme: const TooltipThemeData(triggerMode: TooltipTriggerMode.tap)),
home: Scaffold(
body: Center(
child: Builder(
builder: (BuildContext context) {
return ElevatedButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute<void>(
builder: (_) =>
Scaffold(appBar: AppBar(title: const Text('Second Screen'))),
),
);
},
child: const Text('Go to second screen'),
);
},
),
),
),
),
);
expect(find.text('Second Screen'), findsNothing);
await tester.tap(find.text('Go to second screen'));
await tester.pumpAndSettle();
expect(find.text('Second Screen'), findsOneWidget);
await tester.tap(find.byType(BackButton));
await tester.pumpAndSettle();
expect(find.text('Second Screen'), findsNothing);
});
// Regression test for https://github.com/flutter/flutter/issues/152315
testWidgets(
'Material2 - AppBar back button navigates to previous page on tap with TooltipTriggerMode.tap',
(WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(
useMaterial3: false,
tooltipTheme: const TooltipThemeData(triggerMode: TooltipTriggerMode.tap),
),
home: Scaffold(
body: Center(
child: Builder(
builder: (BuildContext context) {
return ElevatedButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute<void>(
builder: (_) =>
Scaffold(appBar: AppBar(title: const Text('Second Screen'))),
),
);
},
child: const Text('Go to second screen'),
);
},
),
),
),
),
);
expect(find.text('Second Screen'), findsNothing);
await tester.tap(find.text('Go to second screen'));
await tester.pumpAndSettle();
expect(find.text('Second Screen'), findsOneWidget);
await tester.tap(find.byType(BackButton));
await tester.pumpAndSettle();
expect(find.text('Second Screen'), findsNothing);
},
);
testWidgets('AppBar actions padding can be adjusted', (WidgetTester tester) async {
final Key appBarKey = UniqueKey();
final Key actionKey = UniqueKey();
Widget buildAppBar({EdgeInsetsGeometry? actionsPadding}) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
key: appBarKey,
actions: <Widget>[SizedBox.square(key: actionKey, dimension: 40.0)],
actionsPadding: actionsPadding,
),
),
);
}
await tester.pumpWidget(buildAppBar());
// Actions padding default to zero padding.
Offset actionsOffset = tester.getTopRight(find.byKey(actionKey));
final Offset appBarOffset = tester.getTopRight(find.byKey(appBarKey));
expect(appBarOffset.dx - actionsOffset.dx, 0);
const actionsPadding = EdgeInsets.only(right: 8.0);
await tester.pumpWidget(buildAppBar(actionsPadding: actionsPadding));
actionsOffset = tester.getTopRight(find.byKey(actionKey));
expect(actionsOffset.dx, equals(appBarOffset.dx - actionsPadding.right));
});
group('Material 2', () {
testWidgets('Material2 - AppBar draws a light system bar for a dark background', (
WidgetTester tester,
) async {
final darkTheme = ThemeData.dark(useMaterial3: false);
await tester.pumpWidget(
MaterialApp(
theme: darkTheme,
home: Scaffold(appBar: AppBar(title: const Text('test'))),
),
);
expect(darkTheme.colorScheme.brightness, Brightness.dark);
expect(
SystemChrome.latestStyle,
const SystemUiOverlayStyle(
statusBarBrightness: Brightness.dark,
statusBarIconBrightness: Brightness.light,
),
);
});
testWidgets('Material2 - AppBar drawer icon has default color', (WidgetTester tester) async {
final themeData = ThemeData.from(colorScheme: const ColorScheme.light(), useMaterial3: false);
await tester.pumpWidget(
MaterialApp(
theme: themeData,
home: Scaffold(
appBar: AppBar(title: const Text('Howdy!')),
drawer: const Drawer(),
),
),
);
expect(_iconStyle(tester, Icons.menu)?.color, themeData.colorScheme.onPrimary);
});
testWidgets('Material2 - AppBar endDrawer icon has default color', (WidgetTester tester) async {
final themeData = ThemeData.from(colorScheme: const ColorScheme.light(), useMaterial3: false);
await tester.pumpWidget(
MaterialApp(
theme: themeData,
home: Scaffold(
appBar: AppBar(title: const Text('Howdy!')),
endDrawer: const Drawer(),
),
),
);
expect(_iconStyle(tester, Icons.menu)?.color, themeData.colorScheme.onPrimary);
});
testWidgets('Material2 - leading widget extends to edge and is square', (
WidgetTester tester,
) async {
final themeData = ThemeData(platform: TargetPlatform.android, useMaterial3: false);
await tester.pumpWidget(
MaterialApp(
theme: themeData,
home: Scaffold(
appBar: AppBar(
leading: IconButton(icon: const Icon(Icons.menu), onPressed: () {}),
title: const Text('X'),
),
drawer: const Column(), // Doesn't really matter. Triggers a hamburger regardless.
),
),
);
// Default IconButton has a size of (56x56).
final Finder hamburger = find.byType(IconButton);
expect(tester.getTopLeft(hamburger), Offset.zero);
expect(tester.getSize(hamburger), const Size(56.0, 56.0));
await tester.pumpWidget(
MaterialApp(
theme: themeData,
home: Scaffold(
appBar: AppBar(leading: Container(), title: const Text('X')),
),
),
);
// Default leading widget has a size of (56x56).
final Finder leadingBox = find.byType(Container);
expect(tester.getTopLeft(leadingBox), Offset.zero);
expect(tester.getSize(leadingBox), const Size(56.0, 56.0));
// The custom leading widget should still be 56x56 even if its size is smaller.
await tester.pumpWidget(
MaterialApp(
theme: themeData,
home: Scaffold(
appBar: AppBar(
leading: const SizedBox(height: 36, width: 36),
title: const Text('X'),
), // Doesn't really matter. Triggers a hamburger regardless.
),
),
);
final Finder leading = find.byType(SizedBox);
expect(tester.getTopLeft(leading), Offset.zero);
expect(tester.getSize(leading), const Size(56.0, 56.0));
});
testWidgets('Material2 - Action is 4dp from edge and 48dp min', (WidgetTester tester) async {
final theme = ThemeData(platform: TargetPlatform.android, useMaterial3: false);
await tester.pumpWidget(
MaterialApp(
theme: theme,
home: Scaffold(
appBar: AppBar(
title: const Text('X'),
actions: const <Widget>[
IconButton(
icon: Icon(Icons.share),
onPressed: null,
tooltip: 'Share',
iconSize: 20.0,
),
IconButton(icon: Icon(Icons.add), onPressed: null, tooltip: 'Add', iconSize: 60.0),
],
),
),
),
);
final Finder addButton = find.widgetWithIcon(IconButton, Icons.add);
expect(tester.getTopRight(addButton), const Offset(800.0, 0.0));
// It's still the size it was plus the 2 * 8dp padding from IconButton.
expect(tester.getSize(addButton), const Size(60.0 + 2 * 8.0, 56.0));
final Finder shareButton = find.widgetWithIcon(IconButton, Icons.share);
// The 20dp icon is expanded to fill the IconButton's touch target to 48dp.
expect(tester.getSize(shareButton), const Size(48.0, 56.0));
});
testWidgets('Material2 - AppBar uses the specified elevation or defaults to 4.0', (
WidgetTester tester,
) async {
Widget buildAppBar([double? elevation]) {
return MaterialApp(
theme: ThemeData(useMaterial3: false),
home: Scaffold(
appBar: AppBar(title: const Text('Title'), elevation: elevation),
),
);
}
Material getMaterial() => tester.widget<Material>(
find.descendant(of: find.byType(AppBar), matching: find.byType(Material)),
);
// Default elevation should be used for the material.
await tester.pumpWidget(buildAppBar());
expect(getMaterial().elevation, 4);
// AppBar should use the specified elevation.
await tester.pumpWidget(buildAppBar(8.0));
expect(getMaterial().elevation, 8.0);
});
testWidgets('Material2 - AppBar ink splash draw on the correct canvas', (
WidgetTester tester,
) async {
// This is a regression test for https://github.com/flutter/flutter/issues/58665
final Key key = UniqueKey();
await tester.pumpWidget(
MaterialApp(
// Test was designed against InkSplash so need to make sure that is used.
theme: ThemeData(useMaterial3: false, splashFactory: InkSplash.splashFactory),
home: Center(
child: AppBar(
title: const Text('Abc'),
actions: <Widget>[
IconButton(
key: key,
icon: const Icon(Icons.add_circle),
tooltip: 'First button',
onPressed: () {},
),
],
flexibleSpace: DecoratedBox(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: const Alignment(-0.04, 1.0),
colors: <Color>[Colors.blue.shade500, Colors.blue.shade800],
),
),
),
),
),
),
);
final RenderObject painter = tester.renderObject(
find.descendant(
of: find.descendant(of: find.byType(AppBar), matching: find.byType(Stack)),
matching: find.byType(Material),
),
);
await tester.tap(find.byKey(key));
expect(
painter,
paints
..save()
..translate()
..save()
..translate()
..circle(x: 24.0, y: 28.0),
);
});
testWidgets('Material2 - Default status bar color', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
key: GlobalKey(),
theme: ThemeData(useMaterial3: false, appBarTheme: const AppBarThemeData()),
home: Scaffold(appBar: AppBar(title: const Text('title'))),
),
);
expect(SystemChrome.latestStyle!.statusBarColor, null);
});
testWidgets('Material2 - AppBar draws a dark system bar for a light background', (
WidgetTester tester,
) async {
final lightTheme = ThemeData(primarySwatch: Colors.lightBlue, useMaterial3: false);
await tester.pumpWidget(
MaterialApp(
theme: lightTheme,
home: Scaffold(appBar: AppBar(title: const Text('test'))),
),
);
expect(lightTheme.colorScheme.brightness, Brightness.light);
expect(
SystemChrome.latestStyle,
const SystemUiOverlayStyle(
statusBarBrightness: Brightness.light,
statusBarIconBrightness: Brightness.dark,
),
);
});
testWidgets(
'Material2 - Default system bar brightness based on AppBar background color brightness.',
(WidgetTester tester) async {
Widget buildAppBar(ThemeData theme) {
return MaterialApp(
theme: theme,
home: Scaffold(appBar: AppBar(title: const Text('Title'))),
);
}
// Using a light theme.
{
await tester.pumpWidget(buildAppBar(ThemeData(useMaterial3: false)));
final Material appBarMaterial = tester.widget<Material>(
find.descendant(of: find.byType(AppBar), matching: find.byType(Material)),
);
final Brightness appBarBrightness = ThemeData.estimateBrightnessForColor(
appBarMaterial.color!,
);
final Brightness onAppBarBrightness = appBarBrightness == Brightness.light
? Brightness.dark
: Brightness.light;
expect(
SystemChrome.latestStyle,
SystemUiOverlayStyle(
statusBarBrightness: appBarBrightness,
statusBarIconBrightness: onAppBarBrightness,
),
);
}
// Using a dark theme.
{
await tester.pumpWidget(buildAppBar(ThemeData.dark(useMaterial3: false)));
final Material appBarMaterial = tester.widget<Material>(
find.descendant(of: find.byType(AppBar), matching: find.byType(Material)),
);
final Brightness appBarBrightness = ThemeData.estimateBrightnessForColor(
appBarMaterial.color!,
);
final Brightness onAppBarBrightness = appBarBrightness == Brightness.light
? Brightness.dark
: Brightness.light;
expect(
SystemChrome.latestStyle,
SystemUiOverlayStyle(
statusBarBrightness: appBarBrightness,
statusBarIconBrightness: onAppBarBrightness,
),
);
}
},
);
});
}