// Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'dart:typed_data'; import 'package:flutter/cupertino.dart'; import 'package:flutter_test/flutter_test.dart'; import '../image_data.dart'; import '../rendering/mock_canvas.dart'; /// Integration tests testing both [CupertinoPageScaffold] and [CupertinoTabScaffold]. void main() { testWidgets('Contents are behind translucent bar', (WidgetTester tester) async { await tester.pumpWidget( const CupertinoApp( home: CupertinoPageScaffold( // Default nav bar is translucent. navigationBar: CupertinoNavigationBar( middle: Text('Title'), ), child: Center(), ), ), ); expect(tester.getTopLeft(find.byType(Center)), Offset.zero); }); testWidgets('Opaque bar pushes contents down', (WidgetTester tester) async { late BuildContext childContext; await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, child: MediaQuery( data: const MediaQueryData(viewInsets: EdgeInsets.only(top: 20)), child: CupertinoPageScaffold( navigationBar: const CupertinoNavigationBar( middle: Text('Opaque'), backgroundColor: Color(0xFFF8F8F8), ), child: Builder( builder: (BuildContext context) { childContext = context; return Container(); }, ), ), ), )); expect(MediaQuery.of(childContext).padding.top, 0); // The top of the [Container] is 44 px from the top of the screen because // it's pushed down by the opaque navigation bar whose height is 44 px, // and the 20 px [MediaQuery] top padding is fully absorbed by the navigation bar. expect(tester.getRect(find.byType(Container)), const Rect.fromLTRB(0, 44, 800, 600)); }); testWidgets('dark mode and obstruction work', (WidgetTester tester) async { const Color dynamicColor = CupertinoDynamicColor.withBrightness( color: Color(0xFFF8F8F8), darkColor: Color(0xEE333333), ); const CupertinoDynamicColor backgroundColor = CupertinoDynamicColor.withBrightness( color: Color(0xFFFFFFFF), darkColor: Color(0xFF000000), ); late BuildContext childContext; Widget scaffoldWithBrightness(Brightness brightness) { return Directionality( textDirection: TextDirection.ltr, child: MediaQuery( data: MediaQueryData( platformBrightness: brightness, viewInsets: const EdgeInsets.only(top: 20), ), child: CupertinoPageScaffold( backgroundColor: backgroundColor, navigationBar: const CupertinoNavigationBar( middle: Text('Title'), backgroundColor: dynamicColor, ), child: Builder( builder: (BuildContext context) { childContext = context; return Container(); }, ), ), ), ); } await tester.pumpWidget(scaffoldWithBrightness(Brightness.light)); expect(MediaQuery.of(childContext).padding.top, 0); expect(find.byType(CupertinoPageScaffold), paints..rect(color: backgroundColor.color)); await tester.pumpWidget(scaffoldWithBrightness(Brightness.dark)); expect(MediaQuery.of(childContext).padding.top, greaterThan(0)); expect(find.byType(CupertinoPageScaffold), paints..rect(color: backgroundColor.darkColor)); }); testWidgets('Contents padding from viewInsets', (WidgetTester tester) async { await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, child: MediaQuery( data: const MediaQueryData(viewInsets: EdgeInsets.only(bottom: 100.0)), child: CupertinoPageScaffold( navigationBar: const CupertinoNavigationBar( middle: Text('Opaque'), backgroundColor: Color(0xFFF8F8F8), ), child: Container(), ), ), )); expect(tester.getSize(find.byType(Container)).height, 600.0 - 44.0 - 100.0); late BuildContext childContext; await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, child: MediaQuery( data: const MediaQueryData(viewInsets: EdgeInsets.only(bottom: 100.0)), child: CupertinoPageScaffold( navigationBar: const CupertinoNavigationBar( middle: Text('Transparent'), ), child: Builder( builder: (BuildContext context) { childContext = context; return Container(); }, ), ), ), )); expect(tester.getSize(find.byType(Container)).height, 600.0 - 100.0); // The shouldn't see a media query view inset because it was consumed by // the scaffold. expect(MediaQuery.of(childContext).viewInsets.bottom, 0); await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, child: MediaQuery( data: const MediaQueryData(viewInsets: EdgeInsets.only(bottom: 100.0)), child: CupertinoPageScaffold( navigationBar: const CupertinoNavigationBar( middle: Text('Title'), ), resizeToAvoidBottomInset: false, child: Container(), ), ), )); expect(tester.getSize(find.byType(Container)).height, 600.0); }); testWidgets('Contents bottom padding are not consumed by viewInsets when resizeToAvoidBottomInset overridden', (WidgetTester tester) async { const Widget child = CupertinoPageScaffold( resizeToAvoidBottomInset: false, navigationBar: CupertinoNavigationBar( middle: Text('Opaque'), backgroundColor: Color(0xFFF8F8F8), ), child: Placeholder(), ); await tester.pumpWidget( const Directionality( textDirection: TextDirection.ltr, child: MediaQuery( data: MediaQueryData(viewInsets: EdgeInsets.only(bottom: 20.0)), child: child, ), ), ); final Offset initialPoint = tester.getCenter(find.byType(Placeholder)); // Consume bottom padding - as if by the keyboard opening await tester.pumpWidget( const Directionality( textDirection: TextDirection.ltr, child: MediaQuery( data: MediaQueryData( viewPadding: EdgeInsets.only(bottom: 20), viewInsets: EdgeInsets.only(bottom: 300), ), child: child, ), ), ); final Offset finalPoint = tester.getCenter(find.byType(Placeholder)); expect(initialPoint, finalPoint); }); testWidgets('Contents are between opaque bars', (WidgetTester tester) async { const Center page1Center = Center(); await tester.pumpWidget( CupertinoApp( home: CupertinoTabScaffold( tabBar: CupertinoTabBar( backgroundColor: CupertinoColors.white, items: <BottomNavigationBarItem>[ BottomNavigationBarItem( icon: ImageIcon(MemoryImage(Uint8List.fromList(kTransparentImage))), label: 'Tab 1', ), BottomNavigationBarItem( icon: ImageIcon(MemoryImage(Uint8List.fromList(kTransparentImage))), label: 'Tab 2', ), ], ), tabBuilder: (BuildContext context, int index) { return index == 0 ? const CupertinoPageScaffold( navigationBar: CupertinoNavigationBar( backgroundColor: CupertinoColors.white, middle: Text('Title'), ), child: page1Center, ) : const Stack(); }, ), ), ); expect(tester.getSize(find.byWidget(page1Center)).height, 600.0 - 44.0 - 50.0); }); testWidgets('Contents have automatic sliver padding between translucent bars', (WidgetTester tester) async { const SizedBox content = SizedBox(height: 600.0, width: 600.0); await tester.pumpWidget( CupertinoApp( home: MediaQuery( data: const MediaQueryData( padding: EdgeInsets.symmetric(vertical: 20.0), ), child: CupertinoTabScaffold( tabBar: CupertinoTabBar( items: <BottomNavigationBarItem>[ BottomNavigationBarItem( icon: ImageIcon(MemoryImage(Uint8List.fromList(kTransparentImage))), label: 'Tab 1', ), BottomNavigationBarItem( icon: ImageIcon(MemoryImage(Uint8List.fromList(kTransparentImage))), label: 'Tab 2', ), ], ), tabBuilder: (BuildContext context, int index) { return index == 0 ? CupertinoPageScaffold( navigationBar: const CupertinoNavigationBar( middle: Text('Title'), ), child: ListView( children: const <Widget>[ content, ], ), ) : const Stack(); }, ), ), ), ); // List content automatically padded by nav bar and top media query padding. expect(tester.getTopLeft(find.byWidget(content)).dy, 20.0 + 44.0); // Overscroll to the bottom. await tester.drag(find.byWidget(content), const Offset(0.0, -400.0), warnIfMissed: false); // can't be hit (it's empty) but we're aiming for the list really so it doesn't matter // Let it bounce back. await tester.pump(); await tester.pump(const Duration(seconds: 1)); // List content automatically padded by tab bar and bottom media query padding. expect(tester.getBottomLeft(find.byWidget(content)).dy, 600 - 20.0 - 50.0); }); testWidgets('iOS independent tab navigation', (WidgetTester tester) async { // A full on iOS information architecture app with 2 tabs, and 2 pages // in each with independent navigation states. await tester.pumpWidget( CupertinoApp( home: CupertinoTabScaffold( tabBar: CupertinoTabBar( items: <BottomNavigationBarItem>[ BottomNavigationBarItem( icon: ImageIcon(MemoryImage(Uint8List.fromList(kTransparentImage))), label: 'Tab 1', ), BottomNavigationBarItem( icon: ImageIcon(MemoryImage(Uint8List.fromList(kTransparentImage))), label: 'Tab 2', ), ], ), tabBuilder: (BuildContext context, int index) { // For 1-indexed readability. ++index; return CupertinoTabView( builder: (BuildContext context) { return CupertinoPageScaffold( navigationBar: CupertinoNavigationBar( middle: Text('Page 1 of tab $index'), ), child: Center( child: CupertinoButton( child: const Text('Next'), onPressed: () { Navigator.of(context).push( CupertinoPageRoute<void>( builder: (BuildContext context) { return CupertinoPageScaffold( navigationBar: CupertinoNavigationBar( middle: Text('Page 2 of tab $index'), ), child: Center( child: CupertinoButton( child: const Text('Back'), onPressed: () { Navigator.of(context).pop(); }, ), ), ); }, ), ); }, ), ), ); }, ); }, ), ), ); expect(find.text('Page 1 of tab 1'), findsOneWidget); expect(find.text('Page 1 of tab 2'), findsNothing); // Lazy building so not built yet. await tester.tap(find.text('Tab 2')); await tester.pump(); expect(find.text('Page 1 of tab 1'), findsNothing); // It's offstage now. expect(find.text('Page 1 of tab 1', skipOffstage: false), findsOneWidget); expect(find.text('Page 1 of tab 2'), findsOneWidget); // Navigate in tab 2. await tester.tap(find.text('Next')); await tester.pump(); await tester.pump(const Duration(milliseconds: 500)); expect(find.text('Page 2 of tab 2'), isOnstage); expect(find.text('Page 1 of tab 1', skipOffstage: false), isOffstage); await tester.tap(find.text('Tab 1')); await tester.pump(); // Independent navigation stacks. expect(find.text('Page 1 of tab 1'), isOnstage); expect(find.text('Page 2 of tab 2', skipOffstage: false), isOffstage); // Navigate in tab 1. await tester.tap(find.text('Next')); await tester.pump(); await tester.pump(const Duration(milliseconds: 500)); expect(find.text('Page 2 of tab 1'), isOnstage); expect(find.text('Page 2 of tab 2', skipOffstage: false), isOffstage); await tester.tap(find.text('Tab 2')); await tester.pump(); expect(find.text('Page 2 of tab 2'), isOnstage); expect(find.text('Page 2 of tab 1', skipOffstage: false), isOffstage); // Pop in tab 2 await tester.tap(find.text('Back')); await tester.pump(); await tester.pump(const Duration(milliseconds: 500)); expect(find.text('Page 1 of tab 2'), isOnstage); expect(find.text('Page 2 of tab 1', skipOffstage: false), isOffstage); }); testWidgets('Decorated with white background by default', (WidgetTester tester) async { await tester.pumpWidget( const CupertinoApp( home: CupertinoPageScaffold( child: Center(), ), ), ); final DecoratedBox decoratedBox = tester.widgetList(find.byType(DecoratedBox)).elementAt(1) as DecoratedBox; expect(decoratedBox.decoration.runtimeType, BoxDecoration); final BoxDecoration decoration = decoratedBox.decoration as BoxDecoration; expect(decoration.color, isSameColorAs(CupertinoColors.white)); }); testWidgets('Overrides background color', (WidgetTester tester) async { await tester.pumpWidget( const CupertinoApp( home: CupertinoPageScaffold( backgroundColor: Color(0xFF010203), child: Center(), ), ), ); final DecoratedBox decoratedBox = tester.widgetList(find.byType(DecoratedBox)).elementAt(1) as DecoratedBox; expect(decoratedBox.decoration.runtimeType, BoxDecoration); final BoxDecoration decoration = decoratedBox.decoration as BoxDecoration; expect(decoration.color, const Color(0xFF010203)); }); testWidgets('Lists in CupertinoPageScaffold scroll to the top when status bar tapped', (WidgetTester tester) async { await tester.pumpWidget( CupertinoApp( builder: (BuildContext context, Widget? child) { // Acts as a 20px status bar at the root of the app. return MediaQuery( data: MediaQuery.of(context).copyWith(padding: const EdgeInsets.only(top: 20)), child: child!, ); }, home: CupertinoPageScaffold( // Default nav bar is translucent. navigationBar: const CupertinoNavigationBar( middle: Text('Title'), ), child: ListView.builder( itemExtent: 50, itemBuilder: (BuildContext context, int index) => Text(index.toString()), ), ), ), ); // Top media query padding 20 + translucent nav bar 44. expect(tester.getTopLeft(find.text('0')).dy, 64); expect(tester.getTopLeft(find.text('6')).dy, 364); await tester.fling( find.text('5'), // Find some random text on the screen. const Offset(0, -200), 20, ); await tester.pumpAndSettle(); expect(tester.getTopLeft(find.text('6')).dy, moreOrLessEquals(166.833, epsilon: 0.1)); expect(tester.getTopLeft(find.text('12')).dy, moreOrLessEquals(466.8333333333334, epsilon: 0.1)); // The media query top padding is 20. Tapping at 20 should do nothing. await tester.tapAt(const Offset(400, 20)); await tester.pumpAndSettle(); expect(tester.getTopLeft(find.text('6')).dy, moreOrLessEquals(166.833, epsilon: 0.1)); expect(tester.getTopLeft(find.text('12')).dy, moreOrLessEquals(466.8333333333334, epsilon: 0.1)); // Tap 1 pixel higher. await tester.tapAt(const Offset(400, 19)); await tester.pump(); await tester.pump(const Duration(milliseconds: 500)); expect(tester.getTopLeft(find.text('0')).dy, 64); expect(tester.getTopLeft(find.text('6')).dy, 364); expect(find.text('12'), findsNothing); }); testWidgets('resizeToAvoidBottomInset is supported even when no navigationBar', (WidgetTester tester) async { Widget buildFrame(bool showNavigationBar, bool showKeyboard) { return CupertinoApp( home: MediaQuery( data: MediaQueryData( viewPadding: const EdgeInsets.only(bottom: 20), viewInsets: EdgeInsets.only(bottom: showKeyboard ? 300 : 20), ), child: CupertinoPageScaffold( navigationBar: showNavigationBar ? const CupertinoNavigationBar( middle: Text('Title'), ) : null, child: const Center( child: CupertinoTextField(), ), ), ), ); } // When there is a nav bar and no keyboard. await tester.pumpWidget(buildFrame(true, false)); final Offset positionNoInsetWithNavBar = tester.getTopLeft(find.byType(CupertinoTextField)); // When there is a nav bar and keyboard, the CupertinoTextField moves up. await tester.pumpWidget(buildFrame(true, true)); await tester.pumpAndSettle(); final Offset positionWithInsetWithNavBar = tester.getTopLeft(find.byType(CupertinoTextField)); expect(positionWithInsetWithNavBar.dy, lessThan(positionNoInsetWithNavBar.dy)); // When there is no nav bar and no keyboard, the CupertinoTextField is still // centered. await tester.pumpWidget(buildFrame(false, false)); final Offset positionNoInsetNoNavBar = tester.getTopLeft(find.byType(CupertinoTextField)); expect(positionNoInsetNoNavBar, equals(positionNoInsetWithNavBar)); // When there is a keyboard but no nav bar, the CupertinoTextField also // moves up to the same position as when there is a keyboard and nav bar. await tester.pumpWidget(buildFrame(false, true)); await tester.pumpAndSettle(); final Offset positionWithInsetNoNavBar = tester.getTopLeft(find.byType(CupertinoTextField)); expect(positionWithInsetNoNavBar.dy, lessThan(positionNoInsetNoNavBar.dy)); expect(positionWithInsetNoNavBar, equals(positionWithInsetWithNavBar)); }); testWidgets('textScaleFactor is set to 1.0', (WidgetTester tester) async { await tester.pumpWidget( CupertinoApp( home: Builder(builder: (BuildContext context) { return MediaQuery( data: MediaQuery.of(context).copyWith(textScaleFactor: 99), child: const CupertinoPageScaffold( navigationBar: CupertinoNavigationBar( middle: Text('middle'), leading: Text('leading'), trailing: Text('trailing'), ), child: Text('content'), ), ); }), ), ); final Iterable<RichText> richTextList = tester.widgetList<RichText>( find.descendant(of: find.byType(CupertinoNavigationBar), matching: find.byType(RichText)), ); expect(richTextList.length, greaterThan(0)); expect(richTextList.any((RichText text) => text.textScaleFactor != 1), isFalse); expect(tester.widget<RichText>(find.descendant(of: find.text('content'), matching: find.byType(RichText))).textScaleFactor, 99); }); }