// 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(['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(find.byType(Navigator)) .push( CupertinoPageRoute( 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(find.byType(Navigator)) .push( CupertinoPageRoute( 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 [Placeholder()]), ), ), ); scrollController.jumpTo(100.0); await tester.pump(); expect( tester.widget(find.byType(BackdropFilter)), isA().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 [Placeholder()]), ), ), ); scrollController.jumpTo(100.0); await tester.pump(); expect( tester.widget(find.byType(BackdropFilter)), isA().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 [Placeholder()]), ), ), ); expect( test.widget(find.byType(BackdropFilter)), isA().having( (BackdropFilter filter) => filter.enabled, 'filter enabled', false, ), ); scrollController.jumpTo(100.0); await test.pump(); expect( test.widget(find.byType(BackdropFilter)), isA().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: [Placeholder()]), ), ), ); expect( test.widget(find.byType(BackdropFilter)), isA().having( (BackdropFilter filter) => filter.enabled, 'filter enabled', false, ), ); }); testWidgets('Nav bar displays correctly', (WidgetTester tester) async { final navigator = GlobalKey(); await tester.pumpWidget( CupertinoApp( navigatorKey: navigator, home: const CupertinoNavigationBar(middle: Text('Page 1')), ), ); navigator.currentState!.push( CupertinoPageRoute( 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: [ CupertinoSliverNavigationBar( largeTitle: Text('Title'), backgroundColor: Color(0xF0F9F9F9), brightness: Brightness.dark, ), ], ), ), ); expectSameStatusBarStyle(SystemChrome.latestStyle!, SystemUiOverlayStyle.light); await tester.pumpWidget( const CupertinoApp( home: CustomScrollView( slivers: [ 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: [ 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: [ 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 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 opacities = titles.map((Element element) { final RenderAnimatedOpacity renderOpacity = element .findAncestorRenderObjectOfType()!; return renderOpacity.opacity.value; }); expect(opacities, [ 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((Element element) { final RenderAnimatedOpacity renderOpacity = element .findAncestorRenderObjectOfType()!; return renderOpacity.opacity.value; }); expect(opacities, [ 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: [ CupertinoSliverNavigationBar( middle: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 200.0), child: CupertinoSegmentedControl( key: segmentedControlsKey, children: const {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(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 [ 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()!; 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()!; 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()!; 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: [ 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()!; // 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(), 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()!; // 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(find.byType(Navigator)) .push( CupertinoPageRoute( 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(find.byType(Navigator)) .push( CupertinoPageRoute( 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(find.byType(Navigator)) .push( CupertinoSheetRoute( 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(find.byType(Navigator)) .push( CupertinoSheetRoute( builder: (BuildContext context) { return const CupertinoPageScaffold( child: CustomScrollView( slivers: [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(find.byType(Navigator)) .push( CupertinoPageRoute( 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(find.byType(Navigator)) .push( CupertinoPageRoute( 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: [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: [ 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: [ CupertinoSliverNavigationBar(largeTitle: Text('Large Title'), border: null), ], ), ), ), ); expect( semantics.nodesWith( label: 'Large Title', flags: [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.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.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: [ 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: [ 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: [ 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 [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: [ 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: [ 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( 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( 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(find.byType(Navigator)) .push( CupertinoPageRoute( 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(find.byType(Navigator)) .push( CupertinoPageRoute( 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(find.byType(Navigator)) .push( CupertinoPageRoute( 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(find.byType(Navigator)) .push( CupertinoPageRoute( 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: [ 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 barItems = tester.widgetList( 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 ['Search', 'leading', 'middle', 'trailing']) { expect( barItems.any( (RichText t) => t.text.toPlainText() == text && t.textScaler == const TextScaler.linear(scaleFactor), ), isTrue, ); } for (final icon in [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: [ 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 barItems = tester.widgetList( 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 ['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 [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: [ 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 barItems = tester.widgetList( 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 ['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 [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: [ CupertinoSliverNavigationBar.search( largeTitle: Text('Large Title'), searchField: CupertinoSearchTextField(), ), SliverFillRemaining(), ], ), ), ); }, ), ), ); await tester.tap(find.byType(CupertinoSearchTextField), warnIfMissed: false); await tester.pumpAndSettle(); final Iterable barItems = tester.widgetList( 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: [ 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: [ 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(find.byType(Navigator)) .push( CupertinoPageRoute( 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: [ 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: [ 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: [ 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: [ 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: [ 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().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: [ 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 [ 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(); // 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 [ 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(); 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 [ 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(); 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 [ 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(); 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 [ 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(); 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 [ 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(); 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(navBarFinder); final Finder columnFinder = find.descendant(of: navBarFinder, matching: find.byType(Column)); expect(columnFinder, findsOneWidget); final Column column = tester.widget(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(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(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(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(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: [ 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: [ 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: [ 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: [ 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()!; // The opacity of the decoy 'Cancel' button, which is always semi-transparent. final RenderOpacity decoyCancelOpacity = tester .element(find.widgetWithText(CupertinoButton, 'Cancel')) .findAncestorRenderObjectOfType()!; 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()!; 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: [ 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: [ 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: [ 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: [ 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(); } }