aves_mio1/.flutter/packages/flutter/test/cupertino/nav_bar_test.dart
FabioMich66 19a982ede6
Some checks are pending
Quality check / Flutter analysis (push) Waiting to run
Quality check / CodeQL analysis (java-kotlin) (push) Waiting to run
first commit
2026-03-05 15:51:30 +01:00

3359 lines
114 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.
// This file is run as part of a reduced test set in CI on Mac and Windows
// machines.
@Tags(<String>['reduced-test-set'])
library;
import 'package:flutter/cupertino.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import '../widgets/semantics_tester.dart';
int count = 0;
void setWindowToPortrait(WidgetTester tester, {Size size = const Size(2400.0, 3000.0)}) {
tester.view.physicalSize = size;
addTearDown(tester.view.reset);
}
void main() {
testWidgets('Middle still in center with asymmetrical actions', (WidgetTester tester) async {
await tester.pumpWidget(
const CupertinoApp(
home: CupertinoNavigationBar(
leading: CupertinoButton(onPressed: null, child: Text('Something')),
middle: Text('Title'),
),
),
);
// Expect the middle of the title to be exactly in the middle of the screen.
expect(tester.getCenter(find.text('Title')).dx, 400.0);
});
testWidgets('largeTitle is aligned with asymmetrical actions', (WidgetTester tester) async {
await tester.pumpWidget(
const CupertinoApp(
home: CupertinoNavigationBar.large(
leading: CupertinoButton(onPressed: null, child: Text('Something')),
largeTitle: Text('Title'),
),
),
);
expect(tester.getCenter(find.text('Title')).dx, greaterThan(110.0));
expect(tester.getCenter(find.text('Title')).dx, lessThan(111.0));
});
testWidgets('Middle still in center with back button', (WidgetTester tester) async {
await tester.pumpWidget(
const CupertinoApp(home: CupertinoNavigationBar(middle: Text('Title'))),
);
tester
.state<NavigatorState>(find.byType(Navigator))
.push(
CupertinoPageRoute<void>(
builder: (BuildContext context) {
return const CupertinoNavigationBar(middle: Text('Page 2'));
},
),
);
await tester.pump();
await tester.pump(const Duration(milliseconds: 600));
// Expect the middle of the title to be exactly in the middle of the screen.
expect(tester.getCenter(find.text('Page 2')).dx, 400.0);
});
testWidgets('largeTitle still aligned with back button', (WidgetTester tester) async {
await tester.pumpWidget(
const CupertinoApp(home: CupertinoNavigationBar.large(largeTitle: Text('Title'))),
);
tester
.state<NavigatorState>(find.byType(Navigator))
.push(
CupertinoPageRoute<void>(
builder: (BuildContext context) {
return const CupertinoNavigationBar.large(largeTitle: Text('Page 2'));
},
),
);
await tester.pump();
await tester.pump(const Duration(milliseconds: 600));
expect(tester.getCenter(find.text('Page 2')).dx, greaterThan(129.0));
expect(tester.getCenter(find.text('Page 2')).dx, lessThan(130.0));
});
testWidgets(
'Opaque background does not add blur effects, non-opaque background adds blur effects',
(WidgetTester tester) async {
const background = CupertinoDynamicColor.withBrightness(
color: Color(0xFFE5E5E5),
darkColor: Color(0xF3E5E5E5),
);
final scrollController = ScrollController();
addTearDown(scrollController.dispose);
await tester.pumpWidget(
CupertinoApp(
theme: const CupertinoThemeData(brightness: Brightness.light),
home: CupertinoPageScaffold(
navigationBar: const CupertinoNavigationBar(
middle: Text('Title'),
backgroundColor: background,
),
child: ListView(controller: scrollController, children: const <Widget>[Placeholder()]),
),
),
);
scrollController.jumpTo(100.0);
await tester.pump();
expect(
tester.widget(find.byType(BackdropFilter)),
isA<BackdropFilter>().having(
(BackdropFilter filter) => filter.enabled,
'filter enabled',
false,
),
);
expect(find.byType(CupertinoNavigationBar), paints..rect(color: background.color));
await tester.pumpWidget(
CupertinoApp(
theme: const CupertinoThemeData(brightness: Brightness.dark),
home: CupertinoPageScaffold(
navigationBar: const CupertinoNavigationBar(
middle: Text('Title'),
backgroundColor: background,
),
child: ListView(controller: scrollController, children: const <Widget>[Placeholder()]),
),
),
);
scrollController.jumpTo(100.0);
await tester.pump();
expect(
tester.widget(find.byType(BackdropFilter)),
isA<BackdropFilter>().having((BackdropFilter f) => f.enabled, 'filter enabled', true),
);
expect(find.byType(CupertinoNavigationBar), paints..rect(color: background.darkColor));
},
);
testWidgets("Background doesn't add blur effect when no content is scrolled under", (
WidgetTester test,
) async {
final scrollController = ScrollController();
addTearDown(scrollController.dispose);
await test.pumpWidget(
CupertinoApp(
theme: const CupertinoThemeData(brightness: Brightness.light),
home: CupertinoPageScaffold(
navigationBar: const CupertinoNavigationBar(middle: Text('Title')),
child: ListView(controller: scrollController, children: const <Widget>[Placeholder()]),
),
),
);
expect(
test.widget(find.byType(BackdropFilter)),
isA<BackdropFilter>().having(
(BackdropFilter filter) => filter.enabled,
'filter enabled',
false,
),
);
scrollController.jumpTo(100.0);
await test.pump();
expect(
test.widget(find.byType(BackdropFilter)),
isA<BackdropFilter>().having(
(BackdropFilter filter) => filter.enabled,
'filter enabled',
true,
),
);
});
testWidgets('Blur affect is disabled when enableBackgroundFilterBlur is false', (
WidgetTester test,
) async {
await test.pumpWidget(
CupertinoApp(
theme: const CupertinoThemeData(brightness: Brightness.light),
home: CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
backgroundColor: const Color(0xFFFFFFFF).withOpacity(0),
middle: const Text('Title'),
automaticBackgroundVisibility: false,
enableBackgroundFilterBlur: false,
),
child: const Column(children: <Widget>[Placeholder()]),
),
),
);
expect(
test.widget(find.byType(BackdropFilter)),
isA<BackdropFilter>().having(
(BackdropFilter filter) => filter.enabled,
'filter enabled',
false,
),
);
});
testWidgets('Nav bar displays correctly', (WidgetTester tester) async {
final navigator = GlobalKey<NavigatorState>();
await tester.pumpWidget(
CupertinoApp(
navigatorKey: navigator,
home: const CupertinoNavigationBar(middle: Text('Page 1')),
),
);
navigator.currentState!.push<void>(
CupertinoPageRoute<void>(
builder: (BuildContext context) {
return const CupertinoNavigationBar(middle: Text('Page 2'));
},
),
);
await tester.pumpAndSettle();
expect(find.byType(CupertinoNavigationBarBackButton), findsOneWidget);
// Pops the page 2
navigator.currentState!.pop();
await tester.pump();
// Needs another pump to trigger the rebuild;
await tester.pump();
// The back button should still persist;
expect(find.byType(CupertinoNavigationBarBackButton), findsOneWidget);
// The app does not crash
expect(tester.takeException(), isNull);
});
testWidgets('Can specify custom padding', (WidgetTester tester) async {
final Key middleBox = GlobalKey();
await tester.pumpWidget(
CupertinoApp(
home: Align(
alignment: Alignment.topCenter,
child: CupertinoNavigationBar(
leading: const CupertinoButton(onPressed: null, child: Text('Cheetah')),
// Let the box take all the vertical space to test vertical padding but let
// the nav bar position it horizontally.
middle: Align(key: middleBox, widthFactor: 1.0, child: const Text('Title')),
trailing: const CupertinoButton(onPressed: null, child: Text('Puma')),
padding: const EdgeInsetsDirectional.only(
start: 10.0,
end: 20.0,
top: 3.0,
bottom: 4.0,
),
),
),
),
);
expect(tester.getRect(find.byKey(middleBox)).top, 3.0);
// 44 is the standard height of the nav bar.
expect(
tester.getRect(find.byKey(middleBox)).bottom,
// 44 is the standard height of the nav bar.
44.0 - 4.0,
);
expect(tester.getTopLeft(find.widgetWithText(CupertinoButton, 'Cheetah')).dx, 10.0);
expect(tester.getTopRight(find.widgetWithText(CupertinoButton, 'Puma')).dx, 800.0 - 20.0);
// Title is still exactly centered.
expect(tester.getCenter(find.text('Title')).dx, 400.0);
});
// Assert that two SystemUiOverlayStyle instances have the same values for
// status bar properties and that the first instance has no system navigation
// bar properties set.
void expectSameStatusBarStyle(SystemUiOverlayStyle style, SystemUiOverlayStyle expectedStyle) {
expect(style.statusBarColor, expectedStyle.statusBarColor);
expect(style.statusBarBrightness, expectedStyle.statusBarBrightness);
expect(style.statusBarIconBrightness, expectedStyle.statusBarIconBrightness);
expect(style.systemStatusBarContrastEnforced, expectedStyle.systemStatusBarContrastEnforced);
expect(style.systemNavigationBarColor, isNull);
expect(style.systemNavigationBarContrastEnforced, isNull);
expect(style.systemNavigationBarDividerColor, isNull);
expect(style.systemNavigationBarIconBrightness, isNull);
}
// Regression test for https://github.com/flutter/flutter/issues/119270
testWidgets('System navigation bar properties are not overridden', (WidgetTester tester) async {
await tester.pumpWidget(
const CupertinoApp(home: CupertinoNavigationBar(backgroundColor: Color(0xF0F9F9F9))),
);
expectSameStatusBarStyle(SystemChrome.latestStyle!, SystemUiOverlayStyle.dark);
});
testWidgets('Can specify custom brightness', (WidgetTester tester) async {
setWindowToPortrait(tester);
await tester.pumpWidget(
const CupertinoApp(
home: CupertinoNavigationBar(
backgroundColor: Color(0xF0F9F9F9),
brightness: Brightness.dark,
),
),
);
expectSameStatusBarStyle(SystemChrome.latestStyle!, SystemUiOverlayStyle.light);
await tester.pumpWidget(
const CupertinoApp(
home: CupertinoNavigationBar(
backgroundColor: Color(0xF01D1D1D),
brightness: Brightness.light,
),
),
);
expectSameStatusBarStyle(SystemChrome.latestStyle!, SystemUiOverlayStyle.dark);
await tester.pumpWidget(
const CupertinoApp(
home: CustomScrollView(
slivers: <Widget>[
CupertinoSliverNavigationBar(
largeTitle: Text('Title'),
backgroundColor: Color(0xF0F9F9F9),
brightness: Brightness.dark,
),
],
),
),
);
expectSameStatusBarStyle(SystemChrome.latestStyle!, SystemUiOverlayStyle.light);
await tester.pumpWidget(
const CupertinoApp(
home: CustomScrollView(
slivers: <Widget>[
CupertinoSliverNavigationBar(
largeTitle: Text('Title'),
backgroundColor: Color(0xF01D1D1D),
brightness: Brightness.light,
),
],
),
),
);
expectSameStatusBarStyle(SystemChrome.latestStyle!, SystemUiOverlayStyle.dark);
});
testWidgets('Padding works in RTL', (WidgetTester tester) async {
await tester.pumpWidget(
const CupertinoApp(
home: Directionality(
textDirection: TextDirection.rtl,
child: Align(
alignment: Alignment.topCenter,
child: CupertinoNavigationBar(
leading: CupertinoButton(onPressed: null, child: Text('Cheetah')),
// Let the box take all the vertical space to test vertical padding but let
// the nav bar position it horizontally.
middle: Text('Title'),
trailing: CupertinoButton(onPressed: null, child: Text('Puma')),
padding: EdgeInsetsDirectional.only(start: 10.0, end: 20.0),
),
),
),
),
);
expect(tester.getTopRight(find.widgetWithText(CupertinoButton, 'Cheetah')).dx, 800.0 - 10.0);
expect(tester.getTopLeft(find.widgetWithText(CupertinoButton, 'Puma')).dx, 20.0);
// Title is still exactly centered.
expect(tester.getCenter(find.text('Title')).dx, 400.0);
});
testWidgets('Nav bar uses theme defaults', (WidgetTester tester) async {
count = 0x000000;
await tester.pumpWidget(
CupertinoApp(
home: CupertinoNavigationBar(
leading: CupertinoButton(
onPressed: () {},
child: _ExpectStyles(color: CupertinoColors.systemBlue.color, index: 0x000001),
),
middle: const _ExpectStyles(color: CupertinoColors.black, index: 0x000100),
trailing: CupertinoButton(
onPressed: () {},
child: _ExpectStyles(color: CupertinoColors.systemBlue.color, index: 0x010000),
),
),
),
);
expect(count, 0x010101);
});
testWidgets('Nav bar respects themes', (WidgetTester tester) async {
count = 0x000000;
await tester.pumpWidget(
CupertinoApp(
theme: const CupertinoThemeData(brightness: Brightness.dark),
home: CupertinoNavigationBar(
leading: CupertinoButton(
onPressed: () {},
child: _ExpectStyles(color: CupertinoColors.systemBlue.darkColor, index: 0x000001),
),
middle: const _ExpectStyles(color: CupertinoColors.white, index: 0x000100),
trailing: CupertinoButton(
onPressed: () {},
child: _ExpectStyles(color: CupertinoColors.systemBlue.darkColor, index: 0x010000),
),
),
),
);
expect(count, 0x010101);
});
testWidgets('Theme active color can be overridden', (WidgetTester tester) async {
count = 0x000000;
await tester.pumpWidget(
CupertinoApp(
theme: const CupertinoThemeData(primaryColor: Color(0xFF001122)),
home: CupertinoNavigationBar(
leading: CupertinoButton(
onPressed: () {},
child: const _ExpectStyles(color: Color(0xFF001122), index: 0x000001),
),
middle: const _ExpectStyles(color: Color(0xFF000000), index: 0x000100),
trailing: CupertinoButton(
onPressed: () {},
child: const _ExpectStyles(color: Color(0xFF001122), index: 0x010000),
),
),
),
);
expect(count, 0x010101);
});
testWidgets('Nav bar static components respect MediaQueryData', (WidgetTester tester) async {
// This is a regression test for https://github.com/flutter/flutter/issues/174642
const value = 10.0;
void expectCustomMediaQueryData(BuildContext context) {
expect(MediaQuery.platformBrightnessOf(context), Brightness.dark);
expect(MediaQuery.devicePixelRatioOf(context), value);
expect(MediaQuery.viewInsetsOf(context), const EdgeInsets.all(value));
}
await tester.pumpWidget(
CupertinoApp(
home: MediaQuery(
data: const MediaQueryData(
devicePixelRatio: value,
viewInsets: EdgeInsets.all(value),
platformBrightness: Brightness.dark,
),
child: CupertinoNavigationBar(
leading: Builder(
builder: (BuildContext context) {
expectCustomMediaQueryData(context);
return CupertinoButton(onPressed: () {}, child: const Text('leading'));
},
),
middle: Builder(
builder: (BuildContext context) {
expectCustomMediaQueryData(context);
return CupertinoButton(onPressed: () {}, child: const Text('middle'));
},
),
trailing: Builder(
builder: (BuildContext context) {
expectCustomMediaQueryData(context);
return CupertinoButton(onPressed: () {}, child: const Text('trailing'));
},
),
),
),
),
);
expect(find.text('leading'), findsOneWidget);
expect(find.text('middle'), findsOneWidget);
expect(find.text('trailing'), findsOneWidget);
});
testWidgets('No slivers with no large titles', (WidgetTester tester) async {
await tester.pumpWidget(
const CupertinoApp(
home: CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(middle: Text('Title')),
child: Center(),
),
),
);
expect(find.byType(SliverPersistentHeader), findsNothing);
});
testWidgets('Media padding is applied to CupertinoSliverNavigationBar', (
WidgetTester tester,
) async {
final Key leadingKey = GlobalKey();
final Key middleKey = GlobalKey();
final Key trailingKey = GlobalKey();
final Key titleKey = GlobalKey();
await tester.pumpWidget(
CupertinoApp(
home: MediaQuery(
data: const MediaQueryData(
padding: EdgeInsets.only(top: 10.0, left: 20.0, bottom: 30.0, right: 40.0),
),
child: CupertinoPageScaffold(
child: CustomScrollView(
slivers: <Widget>[
CupertinoSliverNavigationBar(
leading: Placeholder(key: leadingKey),
middle: Placeholder(key: middleKey),
largeTitle: Text('Large Title', key: titleKey),
trailing: Placeholder(key: trailingKey),
),
SliverToBoxAdapter(child: Container(height: 1200.0)),
],
),
),
),
),
);
// Media padding applied to leading (T,L), middle (T), trailing (T, R).
expect(tester.getTopLeft(find.byKey(leadingKey)), const Offset(16.0 + 20.0, 10.0));
expect(tester.getRect(find.byKey(middleKey)).top, 10.0);
expect(tester.getTopRight(find.byKey(trailingKey)), const Offset(800.0 - 16.0 - 40.0, 10.0));
// Top and left padding is applied to large title.
expect(tester.getTopLeft(find.byKey(titleKey)), const Offset(16.0 + 20.0, 54.0 + 10.0));
});
testWidgets('Large title nav bar scrolls', (WidgetTester tester) async {
setWindowToPortrait(tester);
final scrollController = ScrollController();
addTearDown(scrollController.dispose);
await tester.pumpWidget(
CupertinoApp(
home: CupertinoPageScaffold(
child: CustomScrollView(
controller: scrollController,
slivers: <Widget>[
const CupertinoSliverNavigationBar(largeTitle: Text('Title')),
SliverToBoxAdapter(child: Container(height: 1200.0)),
],
),
),
),
);
expect(scrollController.offset, 0.0);
expect(tester.getTopLeft(find.byType(NavigationToolbar)).dy, 0.0);
expect(tester.getSize(find.byType(NavigationToolbar)).height, 44.0);
expect(find.text('Title'), findsNWidgets(2)); // Though only one is visible.
List<Element> titles = tester.elementList(find.text('Title')).toList()
..sort((Element a, Element b) {
final aParagraph = a.renderObject! as RenderParagraph;
final bParagraph = b.renderObject! as RenderParagraph;
return aParagraph.text.style!.fontSize!.compareTo(bParagraph.text.style!.fontSize!);
});
Iterable<double> opacities = titles.map<double>((Element element) {
final RenderAnimatedOpacity renderOpacity = element
.findAncestorRenderObjectOfType<RenderAnimatedOpacity>()!;
return renderOpacity.opacity.value;
});
expect(opacities, <double>[
0.0, // Initially the smaller font title is invisible.
1.0, // The larger font title is visible.
]);
expect(tester.getTopLeft(find.widgetWithText(ClipRect, 'Title').first).dy, 44.0);
expect(tester.getSize(find.widgetWithText(ClipRect, 'Title').first).height, 52.0);
scrollController.jumpTo(600.0);
await tester.pump(); // Once to trigger the opacity animation.
await tester.pump(const Duration(milliseconds: 300));
titles = tester.elementList(find.text('Title')).toList()
..sort((Element a, Element b) {
final aParagraph = a.renderObject! as RenderParagraph;
final bParagraph = b.renderObject! as RenderParagraph;
return aParagraph.text.style!.fontSize!.compareTo(bParagraph.text.style!.fontSize!);
});
opacities = titles.map<double>((Element element) {
final RenderAnimatedOpacity renderOpacity = element
.findAncestorRenderObjectOfType<RenderAnimatedOpacity>()!;
return renderOpacity.opacity.value;
});
expect(opacities, <double>[
1.0, // Smaller font title now visible
0.0, // Larger font title invisible.
]);
// The persistent toolbar doesn't move or change size.
expect(tester.getTopLeft(find.byType(NavigationToolbar)).dy, 0.0);
expect(tester.getSize(find.byType(NavigationToolbar)).height, 44.0);
expect(tester.getTopLeft(find.widgetWithText(ClipRect, 'Title').first).dy, 44.0);
// The OverflowBox is squished with the text in it.
expect(tester.getSize(find.widgetWithText(ClipRect, 'Title').first).height, 0.0);
});
testWidgets('User specified middle is always visible in sliver', (WidgetTester tester) async {
setWindowToPortrait(tester);
final scrollController = ScrollController();
addTearDown(scrollController.dispose);
final Key segmentedControlsKey = UniqueKey();
await tester.pumpWidget(
CupertinoApp(
home: CupertinoPageScaffold(
child: CustomScrollView(
controller: scrollController,
slivers: <Widget>[
CupertinoSliverNavigationBar(
middle: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 200.0),
child: CupertinoSegmentedControl<int>(
key: segmentedControlsKey,
children: const <int, Widget>{0: Text('Option A'), 1: Text('Option B')},
onValueChanged: (int selected) {},
groupValue: 0,
),
),
largeTitle: const Text('Title'),
),
SliverToBoxAdapter(child: Container(height: 1200.0)),
],
),
),
),
);
expect(scrollController.offset, 0.0);
expect(tester.getTopLeft(find.byType(NavigationToolbar)).dy, 0.0);
expect(tester.getSize(find.byType(NavigationToolbar)).height, 44.0);
expect(find.text('Title'), findsOneWidget);
expect(tester.getCenter(find.byKey(segmentedControlsKey)).dx, 400.0);
expect(tester.getTopLeft(find.widgetWithText(ClipRect, 'Title').first).dy, 44.0);
expect(tester.getSize(find.widgetWithText(ClipRect, 'Title').first).height, 52.0);
scrollController.jumpTo(600.0);
await tester.pump(); // Once to trigger the opacity animation.
await tester.pump(const Duration(milliseconds: 300));
expect(tester.getCenter(find.byKey(segmentedControlsKey)).dx, 400.0);
// The large title is invisible now.
expect(
tester
.renderObject<RenderAnimatedOpacity>(find.widgetWithText(AnimatedOpacity, 'Title'))
.opacity
.value,
0.0,
);
});
testWidgets(
'User specified middle is only visible when sliver is collapsed if alwaysShowMiddle is false',
(WidgetTester tester) async {
setWindowToPortrait(tester);
final scrollController = ScrollController();
addTearDown(scrollController.dispose);
await tester.pumpWidget(
CupertinoApp(
home: CupertinoPageScaffold(
child: CustomScrollView(
controller: scrollController,
slivers: const <Widget>[
CupertinoSliverNavigationBar(
largeTitle: Text('Large'),
middle: Text('Middle'),
alwaysShowMiddle: false,
),
SliverToBoxAdapter(child: SizedBox(height: 1200.0)),
],
),
),
),
);
expect(scrollController.offset, 0.0);
expect(find.text('Middle'), findsOneWidget);
// Initially (in expanded state) middle widget is not visible.
RenderAnimatedOpacity middleOpacity = tester
.element(find.text('Middle'))
.findAncestorRenderObjectOfType<RenderAnimatedOpacity>()!;
expect(middleOpacity.opacity.value, 0.0);
scrollController.jumpTo(600.0);
await tester.pumpAndSettle();
// Middle widget is visible when nav bar is collapsed.
middleOpacity = tester
.element(find.text('Middle'))
.findAncestorRenderObjectOfType<RenderAnimatedOpacity>()!;
expect(middleOpacity.opacity.value, 1.0);
scrollController.jumpTo(0.0);
await tester.pumpAndSettle();
// Middle widget is not visible when nav bar is again expanded.
middleOpacity = tester
.element(find.text('Middle'))
.findAncestorRenderObjectOfType<RenderAnimatedOpacity>()!;
expect(middleOpacity.opacity.value, 0.0);
},
);
testWidgets('Small title can be overridden', (WidgetTester tester) async {
setWindowToPortrait(tester);
final scrollController = ScrollController();
addTearDown(scrollController.dispose);
await tester.pumpWidget(
CupertinoApp(
home: CupertinoPageScaffold(
child: CustomScrollView(
controller: scrollController,
slivers: <Widget>[
const CupertinoSliverNavigationBar(
middle: Text('Different title'),
largeTitle: Text('Title'),
),
SliverToBoxAdapter(child: Container(height: 1200.0)),
],
),
),
),
);
expect(scrollController.offset, 0.0);
expect(tester.getTopLeft(find.byType(NavigationToolbar)).dy, 0.0);
expect(tester.getSize(find.byType(NavigationToolbar)).height, 44.0);
expect(find.text('Title'), findsOneWidget);
expect(find.text('Different title'), findsOneWidget);
RenderAnimatedOpacity largeTitleOpacity = tester
.element(find.text('Title'))
.findAncestorRenderObjectOfType<RenderAnimatedOpacity>()!;
// Large title initially visible.
expect(largeTitleOpacity.opacity.value, 1.0);
// Middle widget not even wrapped with RenderOpacity, i.e. is always visible.
expect(
tester.element(find.text('Different title')).findAncestorRenderObjectOfType<RenderOpacity>(),
isNull,
);
expect(
tester.getBottomLeft(find.text('Title')).dy,
44.0 + 52.0 - 8.0,
); // Static part + extension - padding.
scrollController.jumpTo(600.0);
await tester.pump(); // Once to trigger the opacity animation.
await tester.pump(const Duration(milliseconds: 300));
largeTitleOpacity = tester
.element(find.text('Title'))
.findAncestorRenderObjectOfType<RenderAnimatedOpacity>()!;
// Large title no longer visible.
expect(largeTitleOpacity.opacity.value, 0.0);
// The persistent toolbar doesn't move or change size.
expect(tester.getTopLeft(find.byType(NavigationToolbar)).dy, 0.0);
expect(tester.getSize(find.byType(NavigationToolbar)).height, 44.0);
expect(tester.getBottomLeft(find.text('Title')).dy, 44.0); // Extension gone.
});
testWidgets('Auto back/cancel button', (WidgetTester tester) async {
await tester.pumpWidget(
const CupertinoApp(home: CupertinoNavigationBar(middle: Text('Home page'))),
);
expect(find.byType(CupertinoButton), findsNothing);
tester
.state<NavigatorState>(find.byType(Navigator))
.push(
CupertinoPageRoute<void>(
builder: (BuildContext context) {
return const CupertinoNavigationBar(middle: Text('Page 2'));
},
),
);
await tester.pump();
await tester.pump(const Duration(milliseconds: 600));
expect(find.byType(CupertinoButton), findsOneWidget);
expect(find.text(String.fromCharCode(CupertinoIcons.back.codePoint)), findsOneWidget);
tester
.state<NavigatorState>(find.byType(Navigator))
.push(
CupertinoPageRoute<void>(
fullscreenDialog: true,
builder: (BuildContext context) {
return const CupertinoNavigationBar(middle: Text('Dialog page'));
},
),
);
await tester.pump();
await tester.pump(const Duration(milliseconds: 600));
expect(find.widgetWithText(CupertinoButton, 'Cancel'), findsOneWidget);
// Test popping goes back correctly.
await tester.tap(find.text('Cancel'));
await tester.pump();
await tester.pump(const Duration(milliseconds: 600));
expect(find.text('Page 2'), findsOneWidget);
await tester.tap(find.text(String.fromCharCode(CupertinoIcons.back.codePoint)));
await tester.pump();
await tester.pump(const Duration(milliseconds: 600));
expect(find.text('Home page'), findsOneWidget);
});
testWidgets('Navigation bars in a CupertinoSheetRoute have no back button', (
WidgetTester tester,
) async {
await tester.pumpWidget(
const CupertinoApp(home: CupertinoNavigationBar(middle: Text('Home page'))),
);
expect(find.byType(CupertinoButton), findsNothing);
tester
.state<NavigatorState>(find.byType(Navigator))
.push(
CupertinoSheetRoute<void>(
builder: (BuildContext context) {
return const CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(middle: Text('Page 2')),
child: Placeholder(),
);
},
),
);
await tester.pump();
await tester.pump(const Duration(milliseconds: 600));
// No back button is found.
expect(find.byType(CupertinoButton), findsNothing);
expect(find.text(String.fromCharCode(CupertinoIcons.back.codePoint)), findsNothing);
tester
.state<NavigatorState>(find.byType(Navigator))
.push(
CupertinoSheetRoute<void>(
builder: (BuildContext context) {
return const CupertinoPageScaffold(
child: CustomScrollView(
slivers: <Widget>[CupertinoSliverNavigationBar(largeTitle: Text('Page 3'))],
),
);
},
),
);
await tester.pump();
await tester.pump(const Duration(milliseconds: 600));
// No back button is found.
expect(find.byType(CupertinoButton), findsNothing);
expect(find.text(String.fromCharCode(CupertinoIcons.back.codePoint)), findsNothing);
});
testWidgets('Long back label turns into "back"', (WidgetTester tester) async {
await tester.pumpWidget(const CupertinoApp(home: Placeholder()));
tester
.state<NavigatorState>(find.byType(Navigator))
.push(
CupertinoPageRoute<void>(
builder: (BuildContext context) {
return const CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(previousPageTitle: '012345678901'),
child: Placeholder(),
);
},
),
);
await tester.pump();
await tester.pump(const Duration(milliseconds: 500));
expect(find.widgetWithText(CupertinoButton, '012345678901'), findsOneWidget);
tester
.state<NavigatorState>(find.byType(Navigator))
.push(
CupertinoPageRoute<void>(
builder: (BuildContext context) {
return const CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(previousPageTitle: '0123456789012'),
child: Placeholder(),
);
},
),
);
await tester.pump();
await tester.pump(const Duration(milliseconds: 500));
expect(find.widgetWithText(CupertinoButton, 'Back'), findsOneWidget);
});
testWidgets('Border should be displayed by default', (WidgetTester tester) async {
await tester.pumpWidget(
const CupertinoApp(home: CupertinoNavigationBar(middle: Text('Title'))),
);
final decoratedBox =
tester
.widgetList(
find.descendant(
of: find.byType(CupertinoNavigationBar),
matching: find.byType(DecoratedBox),
),
)
.first
as DecoratedBox;
expect(decoratedBox.decoration.runtimeType, BoxDecoration);
final decoration = decoratedBox.decoration as BoxDecoration;
expect(decoration.border, isNotNull);
final BorderSide side = decoration.border!.bottom;
expect(side, isNotNull);
});
testWidgets('Overrides border color', (WidgetTester tester) async {
await tester.pumpWidget(
const CupertinoApp(
home: CupertinoNavigationBar(
automaticBackgroundVisibility: false,
middle: Text('Title'),
border: Border(bottom: BorderSide(color: Color(0xFFAABBCC), width: 0.0)),
),
),
);
final decoratedBox =
tester
.widgetList(
find.descendant(
of: find.byType(CupertinoNavigationBar),
matching: find.byType(DecoratedBox),
),
)
.first
as DecoratedBox;
expect(decoratedBox.decoration.runtimeType, BoxDecoration);
final decoration = decoratedBox.decoration as BoxDecoration;
expect(decoration.border, isNotNull);
final BorderSide side = decoration.border!.bottom;
expect(side, isNotNull);
expect(side.color, const Color(0xFFAABBCC));
});
testWidgets('Border should not be displayed when null', (WidgetTester tester) async {
await tester.pumpWidget(
const CupertinoApp(home: CupertinoNavigationBar(middle: Text('Title'), border: null)),
);
final decoratedBox =
tester
.widgetList(
find.descendant(
of: find.byType(CupertinoNavigationBar),
matching: find.byType(DecoratedBox),
),
)
.first
as DecoratedBox;
expect(decoratedBox.decoration.runtimeType, BoxDecoration);
final decoration = decoratedBox.decoration as BoxDecoration;
expect(decoration.border, isNull);
});
testWidgets('Border is displayed by default in sliver nav bar', (WidgetTester tester) async {
await tester.pumpWidget(
const CupertinoApp(
home: CupertinoPageScaffold(
child: CustomScrollView(
slivers: <Widget>[CupertinoSliverNavigationBar(largeTitle: Text('Large Title'))],
),
),
),
);
final decoratedBox =
tester
.widgetList(
find.descendant(
of: find.byType(CupertinoSliverNavigationBar),
matching: find.byType(DecoratedBox),
),
)
.first
as DecoratedBox;
expect(decoratedBox.decoration.runtimeType, BoxDecoration);
final decoration = decoratedBox.decoration as BoxDecoration;
expect(decoration.border, isNotNull);
final BorderSide bottom = decoration.border!.bottom;
expect(bottom, isNotNull);
});
testWidgets('Border is not displayed when null in sliver nav bar', (WidgetTester tester) async {
await tester.pumpWidget(
const CupertinoApp(
home: CupertinoPageScaffold(
child: CustomScrollView(
slivers: <Widget>[
CupertinoSliverNavigationBar(largeTitle: Text('Large Title'), border: null),
],
),
),
),
);
final decoratedBox =
tester
.widgetList(
find.descendant(
of: find.byType(CupertinoSliverNavigationBar),
matching: find.byType(DecoratedBox),
),
)
.first
as DecoratedBox;
expect(decoratedBox.decoration.runtimeType, BoxDecoration);
final decoration = decoratedBox.decoration as BoxDecoration;
expect(decoration.border, isNull);
});
testWidgets('CupertinoSliverNavigationBar has semantics', (WidgetTester tester) async {
final semantics = SemanticsTester(tester);
await tester.pumpWidget(
const CupertinoApp(
home: CupertinoPageScaffold(
child: CustomScrollView(
slivers: <Widget>[
CupertinoSliverNavigationBar(largeTitle: Text('Large Title'), border: null),
],
),
),
),
);
expect(
semantics.nodesWith(
label: 'Large Title',
flags: <SemanticsFlag>[SemanticsFlag.isHeader],
textDirection: TextDirection.ltr,
),
hasLength(1),
);
semantics.dispose();
});
testWidgets('CupertinoNavigationBar has semantics', (WidgetTester tester) async {
final semantics = SemanticsTester(tester);
await tester.pumpWidget(
CupertinoApp(
home: CupertinoPageScaffold(
navigationBar: const CupertinoNavigationBar(middle: Text('Fixed Title')),
child: Container(),
),
),
);
expect(
semantics.nodesWith(
label: 'Fixed Title',
flags: <SemanticsFlag>[SemanticsFlag.isHeader],
textDirection: TextDirection.ltr,
),
hasLength(1),
);
semantics.dispose();
});
testWidgets('Large CupertinoNavigationBar has semantics', (WidgetTester tester) async {
final semantics = SemanticsTester(tester);
await tester.pumpWidget(
CupertinoApp(
home: CupertinoPageScaffold(
navigationBar: const CupertinoNavigationBar.large(largeTitle: Text('Fixed Title')),
child: Container(),
),
),
);
expect(
semantics.nodesWith(
label: 'Fixed Title',
flags: <SemanticsFlag>[SemanticsFlag.isHeader],
textDirection: TextDirection.ltr,
),
hasLength(1),
);
semantics.dispose();
});
testWidgets('Border can be overridden in sliver nav bar', (WidgetTester tester) async {
await tester.pumpWidget(
const CupertinoApp(
home: CupertinoPageScaffold(
child: CustomScrollView(
slivers: <Widget>[
CupertinoSliverNavigationBar(
automaticBackgroundVisibility: false,
largeTitle: Text('Large Title'),
border: Border(bottom: BorderSide(color: Color(0xFFAABBCC), width: 0.0)),
),
],
),
),
),
);
final decoratedBox =
tester
.widgetList(
find.descendant(
of: find.byType(CupertinoSliverNavigationBar),
matching: find.byType(DecoratedBox),
),
)
.first
as DecoratedBox;
expect(decoratedBox.decoration.runtimeType, BoxDecoration);
final decoration = decoratedBox.decoration as BoxDecoration;
expect(decoration.border, isNotNull);
final BorderSide top = decoration.border!.top;
expect(top, isNotNull);
expect(top, BorderSide.none);
final BorderSide bottom = decoration.border!.bottom;
expect(bottom, isNotNull);
expect(bottom.color, const Color(0xFFAABBCC));
});
testWidgets('Static standard title golden', (WidgetTester tester) async {
await tester.pumpWidget(
const CupertinoApp(
home: RepaintBoundary(
child: CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(middle: Text('Bling bling')),
child: Center(),
),
),
),
);
await expectLater(
find.byType(RepaintBoundary).last,
matchesGoldenFile('nav_bar_test.standard_title.png'),
);
});
testWidgets('Static large title golden', (WidgetTester tester) async {
await tester.pumpWidget(
const CupertinoApp(
home: RepaintBoundary(
child: CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar.large(largeTitle: Text('Bling bling')),
child: Center(),
),
),
),
);
await expectLater(
find.byType(RepaintBoundary).last,
matchesGoldenFile('nav_bar_test.large_title.png'),
);
});
testWidgets('Sliver large title golden', (WidgetTester tester) async {
setWindowToPortrait(tester);
await tester.pumpWidget(
CupertinoApp(
home: RepaintBoundary(
child: CupertinoPageScaffold(
child: CustomScrollView(
slivers: <Widget>[
const CupertinoSliverNavigationBar(largeTitle: Text('Bling bling')),
SliverToBoxAdapter(child: Container(height: 1200.0)),
],
),
),
),
),
);
await expectLater(
find.byType(RepaintBoundary).last,
matchesGoldenFile('nav_bar_test.sliver.large_title.png'),
);
});
testWidgets('Sliver middle title golden', (WidgetTester tester) async {
await tester.pumpWidget(
CupertinoApp(
home: RepaintBoundary(
child: CupertinoPageScaffold(
child: CustomScrollView(
slivers: <Widget>[
const CupertinoSliverNavigationBar(
middle: Text('Bling bling'),
largeTitle: Text('Bling bling'),
),
SliverToBoxAdapter(child: Container(height: 1200.0)),
],
),
),
),
),
);
await tester.drag(find.byType(Scrollable), const Offset(0.0, -250.0));
await tester.pump();
await expectLater(
find.byType(RepaintBoundary).last,
matchesGoldenFile('nav_bar_test.sliver.middle_title.png'),
);
});
testWidgets(
'Nav bar background is transparent if `automaticBackgroundVisibility` is true and has no content scrolled under it',
(WidgetTester tester) async {
final scrollController = ScrollController();
addTearDown(scrollController.dispose);
await tester.pumpWidget(
CupertinoApp(
home: CupertinoPageScaffold(
backgroundColor: const Color(0xFFFFFFFF),
navigationBar: const CupertinoNavigationBar(
backgroundColor: Color(0xFFE5E5E5),
border: Border(bottom: BorderSide(color: Color(0xFFAABBCC), width: 0.0)),
middle: Text('Title'),
),
child: ListView(controller: scrollController, children: const <Widget>[Placeholder()]),
),
),
);
expect(scrollController.offset, 0.0);
final decoratedBox =
tester
.widgetList(
find.descendant(
of: find.byType(CupertinoNavigationBar),
matching: find.byType(DecoratedBox),
),
)
.first
as DecoratedBox;
expect(decoratedBox.decoration.runtimeType, BoxDecoration);
final decoration = decoratedBox.decoration as BoxDecoration;
final BorderSide side = decoration.border!.bottom;
expect(side.color.opacity, 0.0);
// Appears transparent since the background color is the same as the scaffold.
expect(find.byType(CupertinoNavigationBar), paints..rect(color: const Color(0xFFFFFFFF)));
scrollController.jumpTo(100.0);
await tester.pump();
final decoratedBoxAfterScroll =
tester
.widgetList(
find.descendant(
of: find.byType(CupertinoNavigationBar),
matching: find.byType(DecoratedBox),
),
)
.first
as DecoratedBox;
expect(decoratedBoxAfterScroll.decoration.runtimeType, BoxDecoration);
final BorderSide borderAfterScroll =
(decoratedBoxAfterScroll.decoration as BoxDecoration).border!.bottom;
expect(borderAfterScroll.color.opacity, 1.0);
expect(find.byType(CupertinoNavigationBar), paints..rect(color: const Color(0xFFE5E5E5)));
},
);
testWidgets(
'automaticBackgroundVisibility parameter has no effect if nav bar is not a child of CupertinoPageScaffold',
(WidgetTester tester) async {
await tester.pumpWidget(
const CupertinoApp(
home: CupertinoNavigationBar(
backgroundColor: Color(0xFFE5E5E5),
border: Border(bottom: BorderSide(color: Color(0xFFAABBCC), width: 0.0)),
middle: Text('Title'),
),
),
);
final decoratedBox =
tester
.widgetList(
find.descendant(
of: find.byType(CupertinoNavigationBar),
matching: find.byType(DecoratedBox),
),
)
.first
as DecoratedBox;
expect(decoratedBox.decoration.runtimeType, BoxDecoration);
final decoration = decoratedBox.decoration as BoxDecoration;
final BorderSide side = decoration.border!.bottom;
expect(side.color, const Color(0xFFAABBCC));
expect(find.byType(CupertinoNavigationBar), paints..rect(color: const Color(0xFFE5E5E5)));
},
);
testWidgets('Nav bar background is always visible if `automaticBackgroundVisibility` is false', (
WidgetTester tester,
) async {
await tester.pumpWidget(
const CupertinoApp(
home: CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
automaticBackgroundVisibility: false,
backgroundColor: Color(0xFFE5E5E5),
border: Border(bottom: BorderSide(color: Color(0xFFAABBCC), width: 0.0)),
middle: Text('Title'),
),
child: Placeholder(),
),
),
);
var decoratedBox =
tester
.widgetList(
find.descendant(
of: find.byType(CupertinoNavigationBar),
matching: find.byType(DecoratedBox),
),
)
.first
as DecoratedBox;
expect(decoratedBox.decoration.runtimeType, BoxDecoration);
var decoration = decoratedBox.decoration as BoxDecoration;
BorderSide side = decoration.border!.bottom;
expect(side.color, const Color(0xFFAABBCC));
expect(find.byType(CupertinoNavigationBar), paints..rect(color: const Color(0xFFE5E5E5)));
await tester.pumpWidget(
CupertinoApp(
home: CupertinoPageScaffold(
child: CustomScrollView(
slivers: <Widget>[
const CupertinoSliverNavigationBar(
automaticBackgroundVisibility: false,
backgroundColor: Color(0xFFE5E5E5),
border: Border(bottom: BorderSide(color: Color(0xFFAABBCC), width: 0.0)),
largeTitle: Text('Title'),
),
SliverToBoxAdapter(child: Container(height: 1200.0)),
],
),
),
),
);
decoratedBox =
tester
.widgetList(
find.descendant(
of: find.byType(CupertinoSliverNavigationBar),
matching: find.byType(DecoratedBox),
),
)
.first
as DecoratedBox;
expect(decoratedBox.decoration.runtimeType, BoxDecoration);
decoration = decoratedBox.decoration as BoxDecoration;
side = decoration.border!.bottom;
expect(side.color, const Color(0xFFAABBCC));
expect(find.byType(CupertinoSliverNavigationBar), paints..rect(color: const Color(0xFFE5E5E5)));
});
testWidgets(
'CupertinoSliverNavigationBar background is transparent if `automaticBackgroundVisibility` is true and has no content scrolled under it',
(WidgetTester tester) async {
final scrollController = ScrollController();
addTearDown(scrollController.dispose);
await tester.pumpWidget(
CupertinoApp(
home: CupertinoPageScaffold(
backgroundColor: const Color(0xFFFFFFFF),
child: CustomScrollView(
controller: scrollController,
slivers: <Widget>[
const CupertinoSliverNavigationBar(
backgroundColor: Color(0xFFE5E5E5),
border: Border(bottom: BorderSide(color: Color(0xFFAABBCC), width: 0.0)),
largeTitle: Text('Title'),
),
SliverToBoxAdapter(child: Container(height: 1200.0)),
],
),
),
),
);
expect(scrollController.offset, 0.0);
final decoratedBox =
tester
.widgetList(
find.descendant(
of: find.byType(CupertinoSliverNavigationBar),
matching: find.byType(DecoratedBox),
),
)
.first
as DecoratedBox;
expect(decoratedBox.decoration.runtimeType, BoxDecoration);
final decoration = decoratedBox.decoration as BoxDecoration;
final BorderSide side = decoration.border!.bottom;
expect(side.color.opacity, 0.0);
// Appears transparent since the background color is the same as the scaffold.
expect(
find.byType(CupertinoSliverNavigationBar),
paints..rect(color: const Color(0xFFFFFFFF)),
);
scrollController.jumpTo(400.0);
await tester.pump();
final decoratedBoxAfterScroll =
tester
.widgetList(
find.descendant(
of: find.byType(CupertinoSliverNavigationBar),
matching: find.byType(DecoratedBox),
),
)
.first
as DecoratedBox;
expect(decoratedBoxAfterScroll.decoration.runtimeType, BoxDecoration);
final BorderSide borderAfterScroll =
(decoratedBoxAfterScroll.decoration as BoxDecoration).border!.bottom;
expect(borderAfterScroll.color.opacity, 1.0);
expect(
find.byType(CupertinoSliverNavigationBar),
paints..rect(color: const Color(0xFFE5E5E5)),
);
},
);
testWidgets('NavBar draws a light system bar for a dark background', (WidgetTester tester) async {
await tester.pumpWidget(
WidgetsApp(
color: const Color(0xFFFFFFFF),
onGenerateRoute: (RouteSettings settings) {
return CupertinoPageRoute<void>(
settings: settings,
builder: (BuildContext context) {
return const CupertinoNavigationBar(
middle: Text('Test'),
backgroundColor: Color(0xFF000000),
);
},
);
},
),
);
expectSameStatusBarStyle(SystemChrome.latestStyle!, SystemUiOverlayStyle.light);
});
testWidgets('NavBar draws a dark system bar for a light background', (WidgetTester tester) async {
await tester.pumpWidget(
WidgetsApp(
color: const Color(0xFFFFFFFF),
onGenerateRoute: (RouteSettings settings) {
return CupertinoPageRoute<void>(
settings: settings,
builder: (BuildContext context) {
return const CupertinoNavigationBar(
middle: Text('Test'),
backgroundColor: Color(0xFFFFFFFF),
);
},
);
},
),
);
expectSameStatusBarStyle(SystemChrome.latestStyle!, SystemUiOverlayStyle.dark);
});
testWidgets(
'CupertinoNavigationBarBackButton shows an error when manually added outside a route',
(WidgetTester tester) async {
await tester.pumpWidget(const CupertinoNavigationBarBackButton());
final dynamic exception = tester.takeException();
expect(exception, isAssertionError);
expect(
exception.toString(),
contains(
'CupertinoNavigationBarBackButton should only be used in routes that can be popped',
),
);
},
);
testWidgets(
'CupertinoNavigationBarBackButton shows an error when placed in a route that cannot be popped',
(WidgetTester tester) async {
await tester.pumpWidget(const CupertinoApp(home: CupertinoNavigationBarBackButton()));
final dynamic exception = tester.takeException();
expect(exception, isAssertionError);
expect(
exception.toString(),
contains(
'CupertinoNavigationBarBackButton should only be used in routes that can be popped',
),
);
},
);
testWidgets(
'CupertinoNavigationBarBackButton with a custom onPressed callback can be placed anywhere',
(WidgetTester tester) async {
var backPressed = false;
await tester.pumpWidget(
CupertinoApp(home: CupertinoNavigationBarBackButton(onPressed: () => backPressed = true)),
);
expect(tester.takeException(), isNull);
expect(find.text(String.fromCharCode(CupertinoIcons.back.codePoint)), findsOneWidget);
await tester.tap(find.byType(CupertinoNavigationBarBackButton));
expect(backPressed, true);
},
);
testWidgets('Manually inserted CupertinoNavigationBarBackButton still automatically '
'show previous page title when possible', (WidgetTester tester) async {
await tester.pumpWidget(const CupertinoApp(home: Placeholder()));
tester
.state<NavigatorState>(find.byType(Navigator))
.push(
CupertinoPageRoute<void>(
title: 'An iPod',
builder: (BuildContext context) {
return const CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(),
child: Placeholder(),
);
},
),
);
await tester.pump();
await tester.pump(const Duration(milliseconds: 500));
tester
.state<NavigatorState>(find.byType(Navigator))
.push(
CupertinoPageRoute<void>(
title: 'A Phone',
builder: (BuildContext context) {
return const CupertinoNavigationBarBackButton();
},
),
);
await tester.pump();
await tester.pump(const Duration(milliseconds: 500));
expect(find.widgetWithText(CupertinoButton, 'An iPod'), findsOneWidget);
});
testWidgets('CupertinoNavigationBarBackButton onPressed overrides default pop behavior', (
WidgetTester tester,
) async {
var backPressed = false;
await tester.pumpWidget(const CupertinoApp(home: Placeholder()));
tester
.state<NavigatorState>(find.byType(Navigator))
.push(
CupertinoPageRoute<void>(
title: 'An iPod',
builder: (BuildContext context) {
return const CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(),
child: Placeholder(),
);
},
),
);
await tester.pump();
await tester.pump(const Duration(milliseconds: 600));
tester
.state<NavigatorState>(find.byType(Navigator))
.push(
CupertinoPageRoute<void>(
title: 'A Phone',
builder: (BuildContext context) {
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
leading: CupertinoNavigationBarBackButton(onPressed: () => backPressed = true),
),
child: const Placeholder(),
);
},
),
);
await tester.pump();
await tester.pump(const Duration(milliseconds: 600));
await tester.tap(find.byType(CupertinoNavigationBarBackButton));
await tester.pump();
await tester.pump(const Duration(milliseconds: 600));
// The second page is still on top and didn't pop.
expect(find.text('A Phone'), findsOneWidget);
// Custom onPressed called.
expect(backPressed, true);
});
testWidgets('Nav bar contents text scale', (WidgetTester tester) async {
setWindowToPortrait(tester);
const scaleFactor = 1.18;
const dampingRatio = 3.0;
const iconSize = 10.0;
await tester.pumpWidget(
CupertinoApp(
home: Builder(
builder: (BuildContext context) {
return MediaQuery.withClampedTextScaling(
minScaleFactor: scaleFactor,
maxScaleFactor: scaleFactor,
child: const CupertinoPageScaffold(
child: CustomScrollView(
slivers: <Widget>[
CupertinoSliverNavigationBar.search(
largeTitle: Text('Large Title'),
leading: Text('leading'),
trailing: Text('trailing'),
middle: Text('middle'),
searchField: CupertinoSearchTextField(
placeholder: 'Search',
prefixIcon: Icon(CupertinoIcons.add),
suffixIcon: Icon(CupertinoIcons.xmark),
suffixMode: OverlayVisibilityMode.always,
itemSize: iconSize,
),
),
SliverFillRemaining(),
],
),
),
);
},
),
),
);
final Iterable<RichText> barItems = tester.widgetList<RichText>(
find.descendant(
of: find.byType(CupertinoSliverNavigationBar),
matching: find.byType(RichText),
),
);
expect(barItems.length, greaterThan(0));
expect(
barItems.any(
(RichText t) =>
t.text.toPlainText() == 'Large Title' &&
t.textScaler == const TextScaler.linear(1.0 + ((scaleFactor - 1.0) / dampingRatio)),
),
isTrue,
);
for (final text in <String>['Search', 'leading', 'middle', 'trailing']) {
expect(
barItems.any(
(RichText t) =>
t.text.toPlainText() == text && t.textScaler == const TextScaler.linear(scaleFactor),
),
isTrue,
);
}
for (final icon in <IconData>[CupertinoIcons.add, CupertinoIcons.xmark]) {
expect(tester.getSize(find.byIcon(icon)), const Size.square(scaleFactor * iconSize));
}
});
testWidgets('Persistent nav bar text scaling clamps to upper bound', (WidgetTester tester) async {
setWindowToPortrait(tester);
const scaleFactor = 10.0;
const maxScaleFactor = 1.235;
const dampingRatio = 3.0;
const iconSize = 10.0;
await tester.pumpWidget(
CupertinoApp(
home: Builder(
builder: (BuildContext context) {
return MediaQuery.withClampedTextScaling(
minScaleFactor: scaleFactor,
maxScaleFactor: scaleFactor,
child: const CupertinoPageScaffold(
child: CustomScrollView(
slivers: <Widget>[
CupertinoSliverNavigationBar.search(
largeTitle: Text('Large Title'),
leading: Text('leading'),
trailing: Text('trailing'),
middle: Text('middle'),
searchField: CupertinoSearchTextField(
placeholder: 'Search',
prefixIcon: Icon(CupertinoIcons.add),
suffixIcon: Icon(CupertinoIcons.xmark),
suffixMode: OverlayVisibilityMode.always,
itemSize: iconSize,
),
),
SliverFillRemaining(),
],
),
),
);
},
),
),
);
final Iterable<RichText> barItems = tester.widgetList<RichText>(
find.descendant(
of: find.byType(CupertinoSliverNavigationBar),
matching: find.byType(RichText),
),
);
expect(barItems.length, greaterThan(0));
expect(
barItems.any(
(RichText t) =>
t.text.toPlainText() == 'Large Title' &&
t.textScaler == const TextScaler.linear(1.0 + ((scaleFactor - 1.0) / dampingRatio)),
),
isTrue,
);
for (final text in <String>['leading', 'middle', 'trailing']) {
expect(
barItems.any(
(RichText t) =>
t.text.toPlainText() == text &&
t.textScaler == const TextScaler.linear(maxScaleFactor),
),
isTrue,
);
}
expect(
barItems.any(
(RichText t) =>
t.text.toPlainText() == 'Search' &&
t.textScaler == const TextScaler.linear(scaleFactor),
),
isTrue,
);
for (final icon in <IconData>[CupertinoIcons.add, CupertinoIcons.xmark]) {
expect(tester.getSize(find.byIcon(icon)), const Size.square(scaleFactor * iconSize));
}
});
testWidgets('Nav bar text scaling clamps to lower bounds', (WidgetTester tester) async {
setWindowToPortrait(tester);
const scaleFactor = 0.5;
const minScaleFactor = 0.9;
const iconSize = 10.0;
await tester.pumpWidget(
CupertinoApp(
home: Builder(
builder: (BuildContext context) {
return MediaQuery.withClampedTextScaling(
minScaleFactor: scaleFactor,
maxScaleFactor: scaleFactor,
child: const CupertinoPageScaffold(
child: CustomScrollView(
slivers: <Widget>[
CupertinoSliverNavigationBar.search(
largeTitle: Text('Large Title'),
leading: Text('leading'),
trailing: Text('trailing'),
middle: Text('middle'),
searchField: CupertinoSearchTextField(
placeholder: 'Search',
prefixIcon: Icon(CupertinoIcons.add),
suffixIcon: Icon(CupertinoIcons.xmark),
suffixMode: OverlayVisibilityMode.always,
itemSize: iconSize,
),
),
SliverFillRemaining(),
],
),
),
);
},
),
),
);
final Iterable<RichText> barItems = tester.widgetList<RichText>(
find.descendant(
of: find.byType(CupertinoSliverNavigationBar),
matching: find.byType(RichText),
),
);
expect(barItems.length, greaterThan(0));
expect(
barItems.any(
(RichText t) =>
t.text.toPlainText() == 'Large Title' &&
t.textScaler == const TextScaler.linear(minScaleFactor),
),
isTrue,
);
for (final text in <String>['leading', 'middle', 'trailing']) {
expect(
barItems.any(
(RichText t) => t.text.toPlainText() == text && t.textScaler == TextScaler.noScaling,
),
isTrue,
);
}
expect(
barItems.any(
(RichText t) =>
t.text.toPlainText() == 'Search' &&
t.textScaler == const TextScaler.linear(scaleFactor),
),
isTrue,
);
for (final icon in <IconData>[CupertinoIcons.add, CupertinoIcons.xmark]) {
expect(tester.getSize(find.byIcon(icon)), const Size.square(scaleFactor * iconSize));
}
});
testWidgets('Active search view cancel button does not text scale', (WidgetTester tester) async {
setWindowToPortrait(tester);
const scaleFactor = 10.0;
await tester.pumpWidget(
CupertinoApp(
home: Builder(
builder: (BuildContext context) {
return MediaQuery.withClampedTextScaling(
minScaleFactor: scaleFactor,
maxScaleFactor: scaleFactor,
child: const CupertinoPageScaffold(
child: CustomScrollView(
slivers: <Widget>[
CupertinoSliverNavigationBar.search(
largeTitle: Text('Large Title'),
searchField: CupertinoSearchTextField(),
),
SliverFillRemaining(),
],
),
),
);
},
),
),
);
await tester.tap(find.byType(CupertinoSearchTextField), warnIfMissed: false);
await tester.pumpAndSettle();
final Iterable<RichText> barItems = tester.widgetList<RichText>(
find.descendant(
of: find.byType(CupertinoSliverNavigationBar),
matching: find.byType(RichText),
),
);
expect(barItems.length, greaterThan(0));
expect(
barItems.any(
(RichText t) =>
t.text.toPlainText() == 'Search' &&
t.textScaler == const TextScaler.linear(scaleFactor),
),
isTrue,
);
expect(
barItems.any(
(RichText t) => t.text.toPlainText() == 'Cancel' && t.textScaler == TextScaler.noScaling,
),
isTrue,
);
});
testWidgets(
'CupertinoSliverNavigationBar stretches upon over-scroll and bounces back once over-scroll ends',
(WidgetTester tester) async {
const trailingText = Text('Bar Button');
const titleText = Text('Large Title');
setWindowToPortrait(tester);
await tester.pumpWidget(
CupertinoApp(
home: CupertinoPageScaffold(
child: CustomScrollView(
slivers: <Widget>[
const CupertinoSliverNavigationBar(
trailing: trailingText,
largeTitle: titleText,
stretch: true,
),
SliverToBoxAdapter(child: Container(height: 1200.0)),
],
),
),
),
);
final Finder trailingTextFinder = find.byWidget(trailingText).first;
final Finder titleTextFinder = find.byWidget(titleText).first;
final Offset initialTrailingTextToLargeTitleOffset =
tester.getTopLeft(trailingTextFinder) - tester.getTopLeft(titleTextFinder);
// Drag for overscroll
await tester.drag(find.byType(Scrollable), const Offset(0.0, 150.0));
await tester.pump();
final Offset stretchedTrailingTextToLargeTitleOffset =
tester.getTopLeft(trailingTextFinder) - tester.getTopLeft(titleTextFinder);
expect(
stretchedTrailingTextToLargeTitleOffset.dy.abs(),
greaterThan(initialTrailingTextToLargeTitleOffset.dy.abs()),
);
// Ensure overscroll retracts to original size after releasing gesture
await tester.pumpAndSettle();
final Offset finalTrailingTextToLargeTitleOffset =
tester.getTopLeft(trailingTextFinder) - tester.getTopLeft(titleTextFinder);
expect(
finalTrailingTextToLargeTitleOffset.dy.abs(),
initialTrailingTextToLargeTitleOffset.dy.abs(),
);
},
);
testWidgets(
'CupertinoSliverNavigationBar does not stretch upon over-scroll if stretch parameter is false',
(WidgetTester tester) async {
const trailingText = Text('Bar Button');
const titleText = Text('Large Title');
await tester.pumpWidget(
CupertinoApp(
home: CupertinoPageScaffold(
child: CustomScrollView(
slivers: <Widget>[
const CupertinoSliverNavigationBar(trailing: trailingText, largeTitle: titleText),
SliverToBoxAdapter(child: Container(height: 1200.0)),
],
),
),
),
);
final Finder trailingTextFinder = find.byWidget(trailingText).first;
final Finder titleTextFinder = find.byWidget(titleText).first;
final Offset initialTrailingTextToLargeTitleOffset =
tester.getTopLeft(trailingTextFinder) - tester.getTopLeft(titleTextFinder);
// Drag for overscroll
await tester.drag(find.byType(Scrollable), const Offset(0.0, 150.0));
await tester.pump();
final Offset stretchedTrailingTextToLargeTitleOffset =
tester.getTopLeft(trailingTextFinder) - tester.getTopLeft(titleTextFinder);
expect(
stretchedTrailingTextToLargeTitleOffset.dy.abs(),
initialTrailingTextToLargeTitleOffset.dy.abs(),
);
// Ensure overscroll is zero after releasing gesture
await tester.pumpAndSettle();
final Offset finalTrailingTextToLargeTitleOffset =
tester.getTopLeft(trailingTextFinder) - tester.getTopLeft(titleTextFinder);
expect(
finalTrailingTextToLargeTitleOffset.dy.abs(),
initialTrailingTextToLargeTitleOffset.dy.abs(),
);
},
);
testWidgets('Null NavigationBar border transition', (WidgetTester tester) async {
// This is a regression test for https://github.com/flutter/flutter/issues/71389
await tester.pumpWidget(
const CupertinoApp(
home: CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(middle: Text('Page 1'), border: null),
child: Placeholder(),
),
),
);
tester
.state<NavigatorState>(find.byType(Navigator))
.push(
CupertinoPageRoute<void>(
builder: (BuildContext context) {
return const CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(middle: Text('Page 2'), border: null),
child: Placeholder(),
);
},
),
);
await tester.pump();
await tester.pump(const Duration(milliseconds: 600));
expect(find.text('Page 1'), findsNothing);
expect(find.text('Page 2'), findsOneWidget);
await tester.tap(find.text(String.fromCharCode(CupertinoIcons.back.codePoint)));
await tester.pump();
await tester.pump(const Duration(milliseconds: 600));
expect(find.text('Page 1'), findsOneWidget);
expect(find.text('Page 2'), findsNothing);
});
testWidgets(
'CupertinoSliverNavigationBar magnifies upon over-scroll and shrinks back once over-scroll ends',
(WidgetTester tester) async {
setWindowToPortrait(tester);
const titleText = Text('Large Title');
await tester.pumpWidget(
CupertinoApp(
home: CupertinoPageScaffold(
child: CustomScrollView(
slivers: <Widget>[
const CupertinoSliverNavigationBar(largeTitle: titleText, stretch: true),
SliverToBoxAdapter(child: Container(height: 1200.0)),
],
),
),
),
);
final Finder titleTextFinder = find.byWidget(titleText).first;
// Gets the height of the large title
final Offset initialLargeTitleTextOffset =
tester.getBottomLeft(titleTextFinder) - tester.getTopLeft(titleTextFinder);
// Drag for overscroll
await tester.drag(find.byType(Scrollable), const Offset(0.0, 150.0));
await tester.pump();
final Offset magnifiedTitleTextOffset =
tester.getBottomLeft(titleTextFinder) - tester.getTopLeft(titleTextFinder);
expect(magnifiedTitleTextOffset.dy.abs(), greaterThan(initialLargeTitleTextOffset.dy.abs()));
// Ensure title text retracts to original size after releasing gesture
await tester.pumpAndSettle();
final Offset finalTitleTextOffset =
tester.getBottomLeft(titleTextFinder) - tester.getTopLeft(titleTextFinder);
expect(finalTitleTextOffset.dy.abs(), initialLargeTitleTextOffset.dy.abs());
},
);
testWidgets('CupertinoSliverNavigationBar large title text does not get clipped when magnified', (
WidgetTester tester,
) async {
const titleText = Text('Very very very long large title');
await tester.pumpWidget(
CupertinoApp(
home: CupertinoPageScaffold(
child: CustomScrollView(
slivers: <Widget>[
const CupertinoSliverNavigationBar(largeTitle: titleText, stretch: true),
SliverToBoxAdapter(child: Container(height: 1200.0)),
],
),
),
),
);
final Finder titleTextFinder = find.byWidget(titleText).first;
// Gets the width of the large title
final Offset initialLargeTitleTextOffset =
tester.getBottomLeft(titleTextFinder) - tester.getBottomRight(titleTextFinder);
// Drag for overscroll
await tester.drag(find.byType(Scrollable), const Offset(0.0, 150.0));
await tester.pump();
final Offset magnifiedTitleTextOffset =
tester.getBottomLeft(titleTextFinder) - tester.getBottomRight(titleTextFinder);
expect(magnifiedTitleTextOffset.dx.abs(), equals(initialLargeTitleTextOffset.dx.abs()));
});
testWidgets('CupertinoSliverNavigationBar large title can be hit tested when magnified', (
WidgetTester tester,
) async {
final scrollController = ScrollController();
addTearDown(scrollController.dispose);
await tester.pumpWidget(
CupertinoApp(
home: CupertinoPageScaffold(
child: CustomScrollView(
controller: scrollController,
slivers: <Widget>[
const CupertinoSliverNavigationBar(largeTitle: Text('Large title'), stretch: true),
SliverToBoxAdapter(child: Container(height: 1200.0)),
],
),
),
),
);
final Finder largeTitleFinder = find.text('Large title').first;
// Drag for overscroll
await tester.drag(find.byType(Scrollable), const Offset(0.0, 250.0));
// Hold position of the scroll view, so the Scrollable unblocks the hit-testing
scrollController.position.hold(() {});
await tester.pumpAndSettle();
expect(largeTitleFinder.hitTestable(), findsOneWidget);
});
testWidgets('NavigationBarBottomMode.automatic mode for bottom', (WidgetTester tester) async {
const persistentHeight = 44.0;
const largeTitleHeight = 44.0;
const bottomHeight = 10.0;
final controller = ScrollController();
setWindowToPortrait(tester);
await tester.pumpWidget(
CupertinoApp(
home: CupertinoPageScaffold(
child: CustomScrollView(
controller: controller,
slivers: <Widget>[
const CupertinoSliverNavigationBar(
largeTitle: Text('Large title'),
bottom: PreferredSize(
preferredSize: Size.fromHeight(bottomHeight),
child: Placeholder(),
),
),
SliverToBoxAdapter(child: Container(height: 1200.0)),
],
),
),
),
);
expect(controller.offset, 0.0);
final Finder largeTitleFinder = find
.ancestor(of: find.text('Large title').first, matching: find.byType(Padding))
.first;
final Finder bottomFinder = find.byType(Placeholder);
// The persistent navigation bar, large title, and search field are all
// visible.
expect(tester.getTopLeft(largeTitleFinder).dy, persistentHeight);
expect(tester.getBottomLeft(largeTitleFinder).dy, persistentHeight + largeTitleHeight);
expect(tester.getTopLeft(bottomFinder).dy, 96.0);
expect(tester.getBottomLeft(bottomFinder).dy, 96.0 + bottomHeight);
// Scroll the length of the navigation bar search text field.
controller.jumpTo(bottomHeight);
await tester.pump();
// The search field is hidden, but the large title remains visible.
expect(tester.getBottomLeft(largeTitleFinder).dy, persistentHeight + largeTitleHeight);
expect(tester.getBottomLeft(bottomFinder).dy - tester.getTopLeft(bottomFinder).dy, 0.0);
// Scroll until the large title scrolls under the persistent navigation bar.
await tester.fling(find.byType(CustomScrollView), const Offset(0.0, -400.0), 10.0);
await tester.pump();
// The large title and search field are both hidden.
expect(tester.getBottomLeft(largeTitleFinder).dy - tester.getTopLeft(bottomFinder).dy, 0.0);
expect(tester.getBottomLeft(bottomFinder).dy - tester.getTopLeft(bottomFinder).dy, 0.0);
controller.dispose();
});
testWidgets('NavigationBarBottomMode.always mode for bottom', (WidgetTester tester) async {
const persistentHeight = 44.0;
const largeTitleHeight = 44.0;
const bottomHeight = 10.0;
setWindowToPortrait(tester);
await tester.pumpWidget(
CupertinoApp(
home: CupertinoPageScaffold(
child: CustomScrollView(
slivers: <Widget>[
const CupertinoSliverNavigationBar(
largeTitle: Text('Large title'),
bottom: PreferredSize(
preferredSize: Size.fromHeight(bottomHeight),
child: Placeholder(),
),
bottomMode: NavigationBarBottomMode.always,
),
SliverToBoxAdapter(child: Container(height: 1200.0)),
],
),
),
),
);
final Finder largeTitleFinder = find
.ancestor(of: find.text('Large title').first, matching: find.byType(Padding))
.first;
final Finder bottomFinder = find.byType(Placeholder);
// The persistent navigation bar, large title, and search field are all
// visible.
expect(tester.getTopLeft(largeTitleFinder).dy, persistentHeight);
expect(tester.getBottomLeft(largeTitleFinder).dy, persistentHeight + largeTitleHeight);
expect(tester.getTopLeft(bottomFinder).dy, 96.0);
expect(tester.getBottomLeft(bottomFinder).dy, 96.0 + bottomHeight);
// Scroll until the large title scrolls under the persistent navigation bar.
await tester.fling(find.byType(CustomScrollView), const Offset(0.0, -400.0), 10.0);
await tester.pump();
// Only the large title is hidden.
expect(tester.getBottomLeft(largeTitleFinder).dy - tester.getTopLeft(bottomFinder).dy, 0.0);
expect(tester.getTopLeft(bottomFinder).dy, persistentHeight);
expect(tester.getBottomLeft(bottomFinder).dy, persistentHeight + bottomHeight);
});
testWidgets('Disallow providing a bottomMode without a corresponding bottom', (
WidgetTester tester,
) async {
expect(
() => const CupertinoSliverNavigationBar(
bottom: PreferredSize(preferredSize: Size.fromHeight(10.0), child: Placeholder()),
bottomMode: NavigationBarBottomMode.automatic,
),
returnsNormally,
);
expect(
() => const CupertinoSliverNavigationBar(
bottom: PreferredSize(preferredSize: Size.fromHeight(10.0), child: Placeholder()),
),
returnsNormally,
);
expect(
() => CupertinoSliverNavigationBar(bottomMode: NavigationBarBottomMode.automatic),
throwsA(
isA<AssertionError>().having(
(AssertionError e) => e.message,
'message',
contains('A bottomMode was provided without a corresponding bottom.'),
),
),
);
});
testWidgets('Overscroll when stretched does not resize bottom in automatic mode', (
WidgetTester tester,
) async {
const bottomHeight = 10.0;
const bottomDisplacement = 96.0;
setWindowToPortrait(tester);
await tester.pumpWidget(
CupertinoApp(
home: CupertinoPageScaffold(
child: CustomScrollView(
slivers: <Widget>[
const CupertinoSliverNavigationBar(
stretch: true,
largeTitle: Text('Large title'),
bottom: PreferredSize(
preferredSize: Size.fromHeight(bottomHeight),
child: Placeholder(),
),
bottomMode: NavigationBarBottomMode.automatic,
),
SliverToBoxAdapter(child: Container(height: 1200.0)),
],
),
),
),
);
final Finder bottomFinder = find.byType(Placeholder);
expect(tester.getTopLeft(bottomFinder).dy, bottomDisplacement);
expect(
tester.getBottomLeft(bottomFinder).dy - tester.getTopLeft(bottomFinder).dy,
bottomHeight,
);
// Overscroll to stretch the navigation bar.
await tester.fling(find.byType(CustomScrollView), const Offset(0.0, 50.0), 10.0);
await tester.pump();
// The bottom stretches without resizing.
expect(tester.getTopLeft(bottomFinder).dy, greaterThan(bottomDisplacement));
expect(
tester.getBottomLeft(bottomFinder).dy - tester.getTopLeft(bottomFinder).dy,
bottomHeight,
);
});
testWidgets(
'Large title snaps up to persistent nav bar when partially scrolled over halfway up',
(WidgetTester tester) async {
final scrollController = ScrollController();
addTearDown(scrollController.dispose);
const largeTitleHeight = 52.0;
setWindowToPortrait(tester);
await tester.pumpWidget(
CupertinoApp(
home: CustomScrollView(
controller: scrollController,
slivers: const <Widget>[
CupertinoSliverNavigationBar(
largeTitle: Text('Large title'),
middle: Text('middle'),
alwaysShowMiddle: false,
),
SliverFillRemaining(child: SizedBox(height: 1000.0)),
],
),
),
);
final RenderAnimatedOpacity? renderOpacity = tester
.element(find.text('middle'))
.findAncestorRenderObjectOfType<RenderAnimatedOpacity>();
// The middle widget is initially invisible.
expect(renderOpacity?.opacity.value, 0.0);
expect(scrollController.offset, 0.0);
// Scroll a little over the halfway point.
final TestGesture scrollGesture = await tester.startGesture(
tester.getCenter(find.byType(Scrollable)),
);
await scrollGesture.moveBy(const Offset(0.0, -(largeTitleHeight / 2) - 1));
await scrollGesture.up();
await tester.pumpAndSettle();
// Expect the large title to snap to the persistent app bar.
expect(scrollController.position.pixels, largeTitleHeight);
expect(renderOpacity?.opacity.value, 1.0);
},
);
testWidgets(
'Large title snaps back to extended height when partially scrolled halfway up or less',
(WidgetTester tester) async {
final scrollController = ScrollController();
addTearDown(scrollController.dispose);
const largeTitleHeight = 52.0;
setWindowToPortrait(tester);
await tester.pumpWidget(
CupertinoApp(
home: CustomScrollView(
controller: scrollController,
slivers: const <Widget>[
CupertinoSliverNavigationBar(
largeTitle: Text('Large title'),
middle: Text('middle'),
alwaysShowMiddle: false,
),
SliverFillRemaining(child: SizedBox(height: 1000.0)),
],
),
),
);
final RenderAnimatedOpacity? renderOpacity = tester
.element(find.text('middle'))
.findAncestorRenderObjectOfType<RenderAnimatedOpacity>();
expect(renderOpacity?.opacity.value, 0.0);
expect(scrollController.offset, 0.0);
// Scroll to the halfway point.
final TestGesture scrollGesture = await tester.startGesture(
tester.getCenter(find.byType(Scrollable)),
);
await scrollGesture.moveBy(const Offset(0.0, -(largeTitleHeight / 2)));
await scrollGesture.up();
await tester.pumpAndSettle();
// Expect the large title to snap back to its extended height.
expect(scrollController.position.pixels, 0.0);
expect(renderOpacity?.opacity.value, 0.0);
},
);
testWidgets(
'Large title and bottom snap up when partially scrolled over halfway up in automatic mode',
(WidgetTester tester) async {
final scrollController = ScrollController();
addTearDown(scrollController.dispose);
const largeTitleHeight = 52.0;
const bottomHeight = 100.0;
setWindowToPortrait(tester);
await tester.pumpWidget(
CupertinoApp(
home: CustomScrollView(
controller: scrollController,
slivers: const <Widget>[
CupertinoSliverNavigationBar(
largeTitle: Text('Large title'),
middle: Text('middle'),
alwaysShowMiddle: false,
bottom: PreferredSize(
preferredSize: Size.fromHeight(bottomHeight),
child: Placeholder(),
),
bottomMode: NavigationBarBottomMode.automatic,
),
SliverFillRemaining(child: SizedBox(height: 1000.0)),
],
),
),
);
final RenderAnimatedOpacity? renderOpacity = tester
.element(find.text('middle'))
.findAncestorRenderObjectOfType<RenderAnimatedOpacity>();
final Finder bottomFinder = find.byType(Placeholder);
expect(renderOpacity?.opacity.value, 0.0);
expect(scrollController.offset, 0.0);
// Scroll to just past the halfway point of the bottom widget.
final TestGesture scrollGesture1 = await tester.startGesture(
tester.getCenter(find.byType(Scrollable)),
);
await scrollGesture1.moveBy(const Offset(0.0, -(bottomHeight / 2) - 1));
await scrollGesture1.up();
await tester.pumpAndSettle();
// Expect the bottom to snap up to the large title.
expect(scrollController.position.pixels, bottomHeight);
expect(tester.getBottomLeft(bottomFinder).dy - tester.getTopLeft(bottomFinder).dy, 0.0);
expect(renderOpacity?.opacity.value, 0.0);
// Scroll to just past the halfway point of the large title.
final TestGesture scrollGesture2 = await tester.startGesture(
tester.getCenter(find.byType(Scrollable)),
);
await scrollGesture2.moveBy(const Offset(0.0, -(largeTitleHeight / 2) - 1));
await scrollGesture2.up();
await tester.pumpAndSettle();
// Expect the large title to snap up to the persistent nav bar.
expect(scrollController.position.pixels, bottomHeight + largeTitleHeight);
expect(renderOpacity?.opacity.value, 1.0);
},
);
testWidgets(
'Large title and bottom snap down when partially scrolled halfway up or less in automatic mode',
(WidgetTester tester) async {
final scrollController = ScrollController();
addTearDown(scrollController.dispose);
const largeTitleHeight = 52.0;
const bottomHeight = 100.0;
setWindowToPortrait(tester);
await tester.pumpWidget(
CupertinoApp(
home: CustomScrollView(
controller: scrollController,
slivers: const <Widget>[
CupertinoSliverNavigationBar(
largeTitle: Text('Large title'),
middle: Text('middle'),
alwaysShowMiddle: false,
bottom: PreferredSize(
preferredSize: Size.fromHeight(bottomHeight),
child: Placeholder(),
),
bottomMode: NavigationBarBottomMode.automatic,
),
SliverFillRemaining(child: SizedBox(height: 1000.0)),
],
),
),
);
final RenderAnimatedOpacity? renderOpacity = tester
.element(find.text('middle'))
.findAncestorRenderObjectOfType<RenderAnimatedOpacity>();
final Finder bottomFinder = find.byType(Placeholder);
expect(renderOpacity?.opacity.value, 0.0);
expect(scrollController.offset, 0.0);
// Scroll to the halfway point of the bottom widget.
final TestGesture scrollGesture1 = await tester.startGesture(
tester.getCenter(find.byType(Scrollable)),
);
await scrollGesture1.moveBy(const Offset(0.0, -bottomHeight / 2));
await scrollGesture1.up();
await tester.pumpAndSettle();
// Expect the bottom to snap back to its extended height.
expect(scrollController.position.pixels, 0.0);
expect(
tester.getBottomLeft(bottomFinder).dy - tester.getTopLeft(bottomFinder).dy,
bottomHeight,
);
expect(renderOpacity?.opacity.value, 0.0);
// Scroll to the halfway point of the large title.
final TestGesture scrollGesture2 = await tester.startGesture(
tester.getCenter(find.byType(Scrollable)),
);
await scrollGesture2.moveBy(const Offset(0.0, -(bottomHeight + largeTitleHeight / 2)));
await scrollGesture2.up();
await tester.pumpAndSettle();
// Expect the large title to snap back to its extended height, which is the
// same scroll offset as the fully-shrunk bottom widget.
expect(scrollController.position.pixels, bottomHeight);
expect(renderOpacity?.opacity.value, 0.0);
},
);
testWidgets(
'Large title and bottom snap up when partially scrolled over halfway up in always mode',
(WidgetTester tester) async {
final scrollController = ScrollController();
addTearDown(scrollController.dispose);
const largeTitleHeight = 52.0;
const bottomHeight = 100.0;
setWindowToPortrait(tester);
await tester.pumpWidget(
CupertinoApp(
home: CustomScrollView(
controller: scrollController,
slivers: const <Widget>[
CupertinoSliverNavigationBar(
largeTitle: Text('Large title'),
middle: Text('middle'),
alwaysShowMiddle: false,
bottom: PreferredSize(
preferredSize: Size.fromHeight(bottomHeight),
child: Placeholder(),
),
bottomMode: NavigationBarBottomMode.always,
),
SliverFillRemaining(child: SizedBox(height: 1000.0)),
],
),
),
);
final RenderAnimatedOpacity? renderOpacity = tester
.element(find.text('middle'))
.findAncestorRenderObjectOfType<RenderAnimatedOpacity>();
final Finder bottomFinder = find.byType(Placeholder);
expect(renderOpacity?.opacity.value, 0.0);
expect(scrollController.position.pixels, 0.0);
// Scroll to just past the halfway point of the large title.
final TestGesture scrollGesture1 = await tester.startGesture(
tester.getCenter(find.byType(Scrollable)),
);
await scrollGesture1.moveBy(const Offset(0.0, -(largeTitleHeight / 2) - 1));
await scrollGesture1.up();
await tester.pumpAndSettle();
// Expect the large title to snap up to the persistent nav bar.
expect(scrollController.position.pixels, largeTitleHeight);
expect(renderOpacity?.opacity.value, 1.0);
// Scroll to just past the halfway point of the bottom widget.
final TestGesture scrollGesture2 = await tester.startGesture(
tester.getCenter(find.byType(Scrollable)),
);
await scrollGesture2.moveBy(const Offset(0.0, -(bottomHeight / 2) - 1));
await scrollGesture2.up();
await tester.pumpAndSettle();
// The bottom widget is still fully extended.
expect(scrollController.position.pixels, largeTitleHeight + (bottomHeight / 2) + 1);
expect(
tester.getBottomLeft(bottomFinder).dy - tester.getTopLeft(bottomFinder).dy,
bottomHeight,
);
expect(renderOpacity?.opacity.value, 1.0);
},
);
testWidgets(
'Large title and bottom snap down when partially scrolled halfway up or less in always mode',
(WidgetTester tester) async {
final scrollController = ScrollController();
addTearDown(scrollController.dispose);
const largeTitleHeight = 52.0;
setWindowToPortrait(tester);
await tester.pumpWidget(
CupertinoApp(
home: CustomScrollView(
controller: scrollController,
slivers: const <Widget>[
CupertinoSliverNavigationBar(
largeTitle: Text('Large title'),
middle: Text('middle'),
alwaysShowMiddle: false,
bottom: PreferredSize(preferredSize: Size.fromHeight(100.0), child: Placeholder()),
bottomMode: NavigationBarBottomMode.always,
),
SliverFillRemaining(child: SizedBox(height: 1000.0)),
],
),
),
);
final RenderAnimatedOpacity? renderOpacity = tester
.element(find.text('middle'))
.findAncestorRenderObjectOfType<RenderAnimatedOpacity>();
expect(renderOpacity?.opacity.value, 0.0);
expect(scrollController.offset, 0.0);
// Scroll to the halfway point of the large title.
final TestGesture scrollGesture1 = await tester.startGesture(
tester.getCenter(find.byType(Scrollable)),
);
await scrollGesture1.moveBy(const Offset(0.0, -largeTitleHeight / 2));
await scrollGesture1.up();
await tester.pumpAndSettle();
// Expect the large title and bottom to snap back to their extended height.
expect(scrollController.position.pixels, 0.0);
expect(renderOpacity?.opacity.value, 0.0);
},
);
testWidgets('CupertinoNavigationBar with bottom widget', (WidgetTester tester) async {
const persistentHeight = 44.0;
const bottomHeight = 10.0;
await tester.pumpWidget(
CupertinoApp(
home: CupertinoPageScaffold(
navigationBar: const CupertinoNavigationBar(
middle: Text('Middle'),
bottom: PreferredSize(
preferredSize: Size.fromHeight(bottomHeight),
child: Placeholder(),
),
),
child: Container(),
),
),
);
final Finder navBarFinder = find.byType(CupertinoNavigationBar);
expect(navBarFinder, findsOneWidget);
final CupertinoNavigationBar navBar = tester.widget<CupertinoNavigationBar>(navBarFinder);
final Finder columnFinder = find.descendant(of: navBarFinder, matching: find.byType(Column));
expect(columnFinder, findsOneWidget);
final Column column = tester.widget<Column>(columnFinder);
expect(column.children.length, 2);
expect(
find.descendant(of: find.byWidget(column.children.first), matching: find.text('Middle')),
findsOneWidget,
);
expect(
find.descendant(of: find.byWidget(column.children.last), matching: find.byType(Placeholder)),
findsOneWidget,
);
expect(navBar.preferredSize.height, persistentHeight + bottomHeight);
});
testWidgets('CupertinoNavigationBar has correct height', (WidgetTester tester) async {
const persistentHeight = 44.0;
await tester.pumpWidget(
const CupertinoApp(
home: CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(middle: Text('Middle')),
child: Placeholder(),
),
),
);
final Finder navBarFinder = find.byType(CupertinoNavigationBar);
expect(navBarFinder, findsOneWidget);
final RenderBox navBarBox = tester.renderObject(navBarFinder);
final CupertinoNavigationBar navBar = tester.widget<CupertinoNavigationBar>(navBarFinder);
// preferredSize should only include persistent height when no large title.
expect(navBar.preferredSize.height, persistentHeight);
// The box size height should match the preferred size height.
expect(navBarBox.size.height, persistentHeight);
});
testWidgets('CupertinoNavigationBar with bottom widget has correct height', (
WidgetTester tester,
) async {
const persistentHeight = 44.0;
const bottomHeight = 20.0;
await tester.pumpWidget(
const CupertinoApp(
home: CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
middle: Text('Middle'),
bottom: PreferredSize(
preferredSize: Size.fromHeight(bottomHeight),
child: Placeholder(),
),
),
child: Placeholder(),
),
),
);
final Finder navBarFinder = find.byType(CupertinoNavigationBar);
expect(navBarFinder, findsOneWidget);
final RenderBox navBarBox = tester.renderObject(navBarFinder);
final CupertinoNavigationBar navBar = tester.widget<CupertinoNavigationBar>(navBarFinder);
// preferredSize should include persistent height and bottom height
expect(navBar.preferredSize.height, persistentHeight + bottomHeight);
// The box size height should match the preferred size height.
expect(navBarBox.size.height, persistentHeight + bottomHeight);
});
testWidgets('CupertinoNavigationBar.large has correct height', (WidgetTester tester) async {
const persistentHeight = 44.0;
const largeTitleExtension = 52.0;
await tester.pumpWidget(
const CupertinoApp(
home: CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar.large(largeTitle: Text('Large Title')),
child: Placeholder(),
),
),
);
final Finder navBarFinder = find.byType(CupertinoNavigationBar);
expect(navBarFinder, findsOneWidget);
final RenderBox navBarBox = tester.renderObject(navBarFinder);
final CupertinoNavigationBar navBar = tester.widget<CupertinoNavigationBar>(navBarFinder);
// preferredSize should include both persistent height and large title extension
expect(navBar.preferredSize.height, persistentHeight + largeTitleExtension);
// The box size height should match the preferred size height.
expect(navBarBox.size.height, persistentHeight + largeTitleExtension);
});
testWidgets('CupertinoNavigationBar.large with bottom widget has correct height', (
WidgetTester tester,
) async {
const persistentHeight = 44.0;
const largeTitleExtension = 52.0;
const bottomHeight = 20.0;
await tester.pumpWidget(
const CupertinoApp(
home: CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar.large(
largeTitle: Text('Large Title'),
bottom: PreferredSize(
preferredSize: Size.fromHeight(bottomHeight),
child: Placeholder(),
),
),
child: Placeholder(),
),
),
);
final Finder navBarFinder = find.byType(CupertinoNavigationBar);
expect(navBarFinder, findsOneWidget);
final RenderBox navBarBox = tester.renderObject(navBarFinder);
final CupertinoNavigationBar navBar = tester.widget<CupertinoNavigationBar>(navBarFinder);
// preferredSize should include persistent height, large title extension, and bottom height
expect(navBar.preferredSize.height, persistentHeight + largeTitleExtension + bottomHeight);
// The box size height should match the preferred size height.
expect(navBarBox.size.height, persistentHeight + largeTitleExtension + bottomHeight);
});
testWidgets('CupertinoSliverNavigationBar.search field collapses nav bar on tap', (
WidgetTester tester,
) async {
setWindowToPortrait(tester);
await tester.pumpWidget(
const CupertinoApp(
home: CustomScrollView(
slivers: <Widget>[
CupertinoSliverNavigationBar.search(
leading: Icon(CupertinoIcons.person_2),
trailing: Icon(CupertinoIcons.add_circled),
largeTitle: Text('Large title'),
middle: Text('middle'),
searchField: CupertinoSearchTextField(),
),
SliverFillRemaining(child: SizedBox(height: 1000.0)),
],
),
),
);
final Finder searchFieldFinder = find.byType(CupertinoSearchTextField);
final Finder largeTitleFinder = find
.ancestor(of: find.text('Large title').first, matching: find.byType(Padding))
.first;
final Finder middleFinder = find
.ancestor(of: find.text('middle').first, matching: find.byType(Padding))
.first;
// Initially, all widgets are visible.
expect(find.byIcon(CupertinoIcons.person_2), findsOneWidget);
expect(find.byIcon(CupertinoIcons.add_circled), findsOneWidget);
expect(largeTitleFinder, findsOneWidget);
expect(middleFinder.hitTestable(), findsOneWidget);
expect(searchFieldFinder, findsOneWidget);
// A decoy 'Cancel' button used in the animation.
expect(find.widgetWithText(CupertinoButton, 'Cancel'), findsOneWidget);
// Tap the search field.
await tester.tap(searchFieldFinder, warnIfMissed: false);
await tester.pump();
// Pump for the duration of the search field animation.
await tester.pump(const Duration(microseconds: 1) + const Duration(milliseconds: 300));
// After tapping, the leading and trailing widgets are removed from the
// widget tree, the large title is collapsed, and middle is hidden
// underneath the navigation bar.
expect(find.byIcon(CupertinoIcons.person_2), findsNothing);
expect(find.byIcon(CupertinoIcons.add_circled), findsNothing);
expect(tester.getBottomRight(largeTitleFinder).dy, 0.0);
expect(middleFinder.hitTestable(), findsNothing);
// Search field and 'Cancel' button are visible.
expect(searchFieldFinder, findsOneWidget);
expect(find.widgetWithText(CupertinoButton, 'Cancel'), findsOneWidget);
// Tap the 'Cancel' button to exit the search view.
await tester.tap(find.widgetWithText(CupertinoButton, 'Cancel'));
await tester.pump();
// Pump for the duration of the search field animation.
await tester.pump(const Duration(microseconds: 1) + const Duration(milliseconds: 300));
// All widgets are visible again.
expect(find.byIcon(CupertinoIcons.person_2), findsOneWidget);
expect(find.byIcon(CupertinoIcons.add_circled), findsOneWidget);
expect(largeTitleFinder, findsOneWidget);
expect(middleFinder.hitTestable(), findsOneWidget);
expect(searchFieldFinder, findsOneWidget);
// A decoy 'Cancel' button used in the animation.
expect(find.widgetWithText(CupertinoButton, 'Cancel'), findsOneWidget);
});
testWidgets('CupertinoSliverNavigationBar.search golden tests', (WidgetTester tester) async {
setWindowToPortrait(tester);
await tester.pumpWidget(
const CupertinoApp(
home: RepaintBoundary(
child: CustomScrollView(
slivers: <Widget>[
CupertinoSliverNavigationBar.search(
largeTitle: Text('Large title'),
searchField: CupertinoSearchTextField(),
),
SliverFillRemaining(child: SizedBox(height: 300.0)),
],
),
),
),
);
await expectLater(
find.byType(CupertinoSliverNavigationBar),
matchesGoldenFile('nav_bar.search.inactive.png'),
);
// Tap the search field.
await tester.tap(find.byType(CupertinoSearchTextField), warnIfMissed: false);
await tester.pump();
// Pump to the end of the animation.
await tester.pump(const Duration(milliseconds: 300));
await expectLater(
find.byType(CupertinoSliverNavigationBar),
matchesGoldenFile('nav_bar.search.active.png'),
);
// Tap the 'Cancel' button to exit the search view.
await tester.tap(find.widgetWithText(CupertinoButton, 'Cancel'));
await tester.pump();
// Pump for the duration of the search field animation.
await tester.pump(const Duration(milliseconds: 300));
await expectLater(
find.byType(CupertinoSliverNavigationBar),
matchesGoldenFile('nav_bar.search.inactive.png'),
);
});
testWidgets('onSearchableBottomTap callback', (WidgetTester tester) async {
setWindowToPortrait(tester);
const activeSearchColor = Color(0x0000000A);
const inactiveSearchColor = Color(0x0000000B);
var isSearchActive = false;
var text = '';
await tester.pumpWidget(
CupertinoApp(
home: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return CustomScrollView(
slivers: <Widget>[
CupertinoSliverNavigationBar.search(
searchField: CupertinoSearchTextField(
onChanged: (String value) {
setState(() {
text = 'The text has changed to: $value';
});
},
),
onSearchableBottomTap: (bool value) {
setState(() {
isSearchActive = value;
});
},
largeTitle: const Text('Large title'),
middle: const Text('middle'),
bottomMode: NavigationBarBottomMode.always,
),
SliverFillRemaining(
child: ColoredBox(
color: isSearchActive ? activeSearchColor : inactiveSearchColor,
child: Text(text),
),
),
],
);
},
),
),
);
// Initially, all widgets are visible.
expect(find.text('Large title'), findsOneWidget);
expect(find.text('middle'), findsOneWidget);
expect(find.widgetWithText(CupertinoSearchTextField, 'Search'), findsOneWidget);
expect(
find.byWidgetPredicate((Widget widget) {
return widget is ColoredBox && widget.color == inactiveSearchColor;
}),
findsOneWidget,
);
// Tap the search field.
await tester.tap(find.widgetWithText(CupertinoSearchTextField, 'Search'), warnIfMissed: false);
await tester.pumpAndSettle();
// Search field and 'Cancel' button should be visible.
expect(isSearchActive, true);
expect(find.widgetWithText(CupertinoSearchTextField, 'Search'), findsOneWidget);
expect(find.widgetWithText(CupertinoButton, 'Cancel'), findsOneWidget);
expect(
find.byWidgetPredicate((Widget widget) {
return widget is ColoredBox && widget.color == activeSearchColor;
}),
findsOneWidget,
);
// Enter text into search field to search.
await tester.enterText(find.widgetWithText(CupertinoSearchTextField, 'Search'), 'aaa');
await tester.pumpAndSettle();
// The entered text is shown in the search view.
expect(find.text('The text has changed to: aaa'), findsOne);
});
testWidgets(
'CupertinoSliverNavigationBar.search large title and cancel buttons fade during search animation',
(WidgetTester tester) async {
setWindowToPortrait(tester);
await tester.pumpWidget(
const CupertinoApp(
home: CustomScrollView(
slivers: <Widget>[
CupertinoSliverNavigationBar.search(
largeTitle: Text('Large title'),
middle: Text('Middle'),
searchField: CupertinoSearchTextField(),
),
SliverFillRemaining(child: SizedBox(height: 1000.0)),
],
),
),
);
// Initially, all widgets are visible.
final RenderAnimatedOpacity largeTitleOpacity = tester
.element(find.text('Large title'))
.findAncestorRenderObjectOfType<RenderAnimatedOpacity>()!;
// The opacity of the decoy 'Cancel' button, which is always semi-transparent.
final RenderOpacity decoyCancelOpacity = tester
.element(find.widgetWithText(CupertinoButton, 'Cancel'))
.findAncestorRenderObjectOfType<RenderOpacity>()!;
expect(largeTitleOpacity.opacity.value, 1.0);
expect(decoyCancelOpacity.opacity, 0.4);
// Tap the search field, and pump up until partway through the animation.
await tester.tap(
find.widgetWithText(CupertinoSearchTextField, 'Search'),
warnIfMissed: false,
);
await tester.pump();
await tester.pump(const Duration(milliseconds: 50));
// During the inactive-to-active search animation, the large title fades
// out and the 'Cancel' button remains at a constant semi-transparent
// value.
expect(largeTitleOpacity.opacity.value, lessThan(1.0));
expect(largeTitleOpacity.opacity.value, greaterThan(0.0));
expect(decoyCancelOpacity.opacity, 0.4);
// At the end of the animation, the large title has completely faded out.
await tester.pump(const Duration(milliseconds: 300));
expect(largeTitleOpacity.opacity.value, 0.0);
expect(decoyCancelOpacity.opacity, 0.4);
// The opacity of the tappable 'Cancel' button.
final RenderAnimatedOpacity cancelOpacity = tester
.element(find.widgetWithText(CupertinoButton, 'Cancel'))
.findAncestorRenderObjectOfType<RenderAnimatedOpacity>()!;
expect(cancelOpacity.opacity.value, 1.0);
// Tap the 'Cancel' button, and pump up until partway through the animation.
await tester.tap(find.widgetWithText(CupertinoButton, 'Cancel'));
await tester.pump();
await tester.pump(const Duration(milliseconds: 50));
// During the active-to-inactive search animation, the large title fades
// in and the 'Cancel' button fades out.
expect(largeTitleOpacity.opacity.value, lessThan(1.0));
expect(largeTitleOpacity.opacity.value, greaterThan(0.0));
expect(cancelOpacity.opacity.value, lessThan(1.0));
expect(cancelOpacity.opacity.value, greaterThan(0.0));
// At the end of the animation, the large title has completely faded in
// and the 'Cancel' button has completely faded out.
await tester.pump(const Duration(milliseconds: 300));
expect(largeTitleOpacity.opacity.value, 1.0);
expect(cancelOpacity.opacity.value, 0.0);
},
);
testWidgets('Large title is hidden if middle is provided in landscape mode', (
WidgetTester tester,
) async {
const largeTitle = 'Large title';
const middle = 'Middle';
await tester.pumpWidget(
const CupertinoApp(
home: CustomScrollView(
slivers: <Widget>[
CupertinoSliverNavigationBar.search(
largeTitle: Text(largeTitle),
middle: Text(middle),
searchField: CupertinoSearchTextField(),
),
SliverFillRemaining(child: SizedBox(height: 1000.0)),
],
),
),
);
expect(find.text(largeTitle), findsNothing);
expect(find.text(middle), findsOneWidget);
expect(find.byType(CupertinoSearchTextField), findsOneWidget);
});
testWidgets('Large title is shown in middle position in landscape mode', (
WidgetTester tester,
) async {
const largeTitle = 'Large title';
await tester.pumpWidget(
const CupertinoApp(
home: CustomScrollView(
slivers: <Widget>[
CupertinoSliverNavigationBar.search(
largeTitle: Text(largeTitle),
searchField: CupertinoSearchTextField(),
),
SliverFillRemaining(child: SizedBox(height: 1000.0)),
],
),
),
);
expect(find.text(largeTitle), findsOneWidget);
expect(find.byType(CupertinoSearchTextField), findsOneWidget);
});
testWidgets(
'CupertinoSliverNavigationBar does not enter infinite animation loop when target exceeds maxScrollExtent and prevents buttons from being tapped',
(WidgetTester tester) async {
const largeTitleHeight = 52.0;
final scrollController = ScrollController();
addTearDown(scrollController.dispose);
tester.view.devicePixelRatio = 1;
tester.binding.platformDispatcher.textScaleFactorTestValue = 1;
setWindowToPortrait(tester, size: const Size(402, 874));
var count = 0;
await tester.pumpWidget(
CupertinoApp(
home: CustomScrollView(
controller: scrollController,
slivers: <Widget>[
const CupertinoSliverNavigationBar(largeTitle: Text('Large Title')),
SliverToBoxAdapter(
child: SizedBox(
// This height will trigger the issue if the target exceeds maxScrollExtent.
height: 805,
child: Center(
child: CupertinoButton(
child: const Text('Press me!'),
onPressed: () => count++,
),
),
),
),
],
),
),
);
await tester.pumpAndSettle();
// Verify the button is present.
expect(find.widgetWithText(CupertinoButton, 'Press me!'), findsOneWidget);
// Tap the button.
await tester.tap(find.widgetWithText(CupertinoButton, 'Press me!'));
await tester.pump();
// Check if the counter has increased.
expect(count, 1);
// Scroll a little over the halfway point.
final TestGesture scrollGesture = await tester.startGesture(
tester.getCenter(find.byType(Scrollable)),
);
await scrollGesture.moveBy(const Offset(0.0, -(largeTitleHeight / 2) - 1));
await scrollGesture.up();
// The crux of this test: this should NOT time out or throw an error.
await tester.pumpAndSettle();
// Tap the button.
await tester.tap(find.widgetWithText(CupertinoButton, 'Press me!'));
await tester.pump();
// Check if the counter has increased.
expect(count, 2);
},
);
testWidgets('Sliver nav bar middle can be updated', (WidgetTester tester) async {
setWindowToPortrait(tester);
var middle = 'First';
late StateSetter setState;
await tester.pumpWidget(
CupertinoApp(
home: StatefulBuilder(
builder: (BuildContext context, StateSetter stateSetter) {
setState = stateSetter;
return CustomScrollView(
slivers: <Widget>[
CupertinoSliverNavigationBar(
largeTitle: const Text('Large title'),
middle: Text(middle),
),
const SliverFillRemaining(child: SizedBox(height: 1000.0)),
],
);
},
),
),
);
expect(find.text('First'), findsOneWidget);
expect(find.text('Second'), findsNothing);
setState(() {
middle = 'Second';
});
await tester.pump();
expect(find.text('First'), findsNothing);
expect(find.text('Second'), findsOneWidget);
});
testWidgets('CupertinoNavigationBar does not crash at zero area', (WidgetTester tester) async {
await tester.pumpWidget(
const CupertinoApp(
home: Center(child: SizedBox.shrink(child: CupertinoNavigationBar())),
),
);
expect(tester.getSize(find.byType(CupertinoNavigationBar)), Size.zero);
});
}
class _ExpectStyles extends StatelessWidget {
const _ExpectStyles({required this.color, required this.index});
final Color color;
final int index;
@override
Widget build(BuildContext context) {
final TextStyle style = DefaultTextStyle.of(context).style;
expect(style.color, isSameColorAs(color));
expect(style.fontFamily, 'CupertinoSystemText');
expect(style.fontSize, 17.0);
expect(style.letterSpacing, -0.41);
count += index;
return Container();
}
}