// Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_test/flutter_test.dart'; import '../widgets/navigator_utils.dart'; // Matches _kTopGapRatio in cupertino/sheet.dart. const double _kTopGapRatio = 0.08; void main() { testWidgets('Sheet route does not cover the whole screen', (WidgetTester tester) async { final GlobalKey scaffoldKey = GlobalKey(); await tester.pumpWidget( CupertinoApp( home: CupertinoPageScaffold( key: scaffoldKey, child: Center( child: Column( children: [ const Text('Page 1'), CupertinoButton( onPressed: () { Navigator.push( scaffoldKey.currentContext!, CupertinoSheetRoute( builder: (BuildContext context) { return const CupertinoPageScaffold(child: Text('Page 2')); }, ), ); }, child: const Text('Push Page 2'), ), ], ), ), ), ), ); expect(find.text('Page 1'), findsOneWidget); expect(find.text('Page 2'), findsNothing); await tester.tap(find.text('Push Page 2')); await tester.pumpAndSettle(); expect(find.text('Page 2'), findsOneWidget); expect( tester .getTopLeft( find.ancestor(of: find.text('Page 2'), matching: find.byType(CupertinoPageScaffold)), ) .dy, greaterThan(0.0), ); }); testWidgets('showDragHandle adds a drag handle to the top of the sheet', ( WidgetTester tester, ) async { final GlobalKey scaffoldKey = GlobalKey(); await tester.pumpWidget( CupertinoApp( home: CupertinoPageScaffold( key: scaffoldKey, child: Center( child: Column( children: [ const Text('Page 1'), CupertinoButton( onPressed: () { Navigator.push( scaffoldKey.currentContext!, CupertinoSheetRoute( showDragHandle: true, builder: (BuildContext context) { return const CupertinoPageScaffold(child: Text('Page 2')); }, ), ); }, child: const Text('Push Page 2'), ), ], ), ), ), ), ); expect(find.text('Page 1'), findsOneWidget); expect(find.text('Page 2'), findsNothing); await tester.tap(find.text('Push Page 2')); await tester.pumpAndSettle(); expect(find.text('Page 2'), findsOneWidget); final Finder dragHandleFinder = find.byWidgetPredicate((Widget widget) { return widget is DecoratedBox && widget.decoration is ShapeDecoration && (widget.decoration as ShapeDecoration).color == CupertinoColors.tertiaryLabel; }); expect(dragHandleFinder, findsOneWidget); }); testWidgets('showDragHandle adds a MediaQuery padding so content can render below the handle', ( WidgetTester tester, ) async { final GlobalKey scaffoldKey = GlobalKey(); await tester.pumpWidget( CupertinoApp( home: CupertinoPageScaffold( key: scaffoldKey, child: Center( child: Column( children: [ const Text('Page 1'), CupertinoButton( onPressed: () { Navigator.push( scaffoldKey.currentContext!, CupertinoSheetRoute( showDragHandle: true, builder: (BuildContext context) { return const CupertinoPageScaffold( child: SafeArea(child: Text('Page 2')), ); }, ), ); }, child: const Text('Push Page 2'), ), ], ), ), ), ), ); expect(find.text('Page 1'), findsOneWidget); expect(find.text('Page 2'), findsNothing); await tester.tap(find.text('Push Page 2')); await tester.pumpAndSettle(); expect(find.text('Page 2'), findsOneWidget); final Finder dragHandleFinder = find.byWidgetPredicate((Widget widget) { return widget is DecoratedBox && widget.decoration is ShapeDecoration && (widget.decoration as ShapeDecoration).color == CupertinoColors.tertiaryLabel; }); final Offset dragHandleOffset = tester.getTopLeft(dragHandleFinder); final Offset sheetContentOffset = tester.getTopLeft(find.text('Page 2')); expect(sheetContentOffset.dy, greaterThan(dragHandleOffset.dy)); }); testWidgets('Previous route moves slight downward when sheet route is pushed', ( WidgetTester tester, ) async { final GlobalKey scaffoldKey = GlobalKey(); await tester.pumpWidget( CupertinoApp( home: CupertinoPageScaffold( key: scaffoldKey, child: Column( children: [ const Text('Page 1'), CupertinoButton( onPressed: () { Navigator.push( scaffoldKey.currentContext!, CupertinoSheetRoute( builder: (BuildContext context) { return const CupertinoPageScaffold(child: Text('Page 2')); }, ), ); }, child: const Text('Push Page 2'), ), ], ), ), ), ); expect(find.text('Page 1'), findsOneWidget); expect( tester .getTopLeft( find.ancestor(of: find.text('Page 1'), matching: find.byType(CupertinoPageScaffold)), ) .dy, equals(0.0), ); expect(find.text('Page 2'), findsNothing); await tester.tap(find.text('Push Page 2')); await tester.pumpAndSettle(); // Previous page is still visible behind the new sheet. expect(find.text('Page 1'), findsOneWidget); final Offset pageOneOffset = tester.getTopLeft( find.ancestor(of: find.text('Page 1'), matching: find.byType(CupertinoPageScaffold)), ); expect(pageOneOffset.dy, greaterThan(0.0)); expect(pageOneOffset.dx, greaterThan(0.0)); expect(find.text('Page 2'), findsOneWidget); final double pageTwoYOffset = tester .getTopLeft( find.ancestor(of: find.text('Page 2'), matching: find.byType(CupertinoPageScaffold)), ) .dy; expect(pageTwoYOffset, greaterThan(pageOneOffset.dy)); }); testWidgets('If a sheet covers another sheet, then the previous sheet moves slightly upwards', ( WidgetTester tester, ) async { final GlobalKey scaffoldKey = GlobalKey(); await tester.pumpWidget( CupertinoApp( home: CupertinoPageScaffold( key: scaffoldKey, child: Column( children: [ const Text('Page 1'), CupertinoButton( onPressed: () { Navigator.push( scaffoldKey.currentContext!, CupertinoSheetRoute( builder: (BuildContext context) { return CupertinoPageScaffold( child: Column( children: [ const Text('Page 2'), CupertinoButton( onPressed: () { Navigator.push( scaffoldKey.currentContext!, CupertinoSheetRoute( builder: (BuildContext context) { return const CupertinoPageScaffold(child: Text('Page 3')); }, ), ); }, child: const Text('Push Page 3'), ), ], ), ); }, ), ); }, child: const Text('Push Page 2'), ), ], ), ), ), ); expect(find.text('Page 1'), findsOneWidget); expect( tester .getTopLeft( find.ancestor(of: find.text('Page 1'), matching: find.byType(CupertinoPageScaffold)), ) .dy, equals(0.0), ); expect(find.text('Page 2'), findsNothing); expect(find.text('Page 3'), findsNothing); await tester.tap(find.text('Push Page 2')); await tester.pumpAndSettle(); expect(find.text('Page 2'), findsOneWidget); expect(find.text('Page 3'), findsNothing); final double previousPageTwoDY = tester .getTopLeft( find.ancestor(of: find.text('Page 2'), matching: find.byType(CupertinoPageScaffold)), ) .dy; await tester.tap(find.text('Push Page 3')); await tester.pumpAndSettle(); expect(find.text('Page 3'), findsOneWidget); expect(previousPageTwoDY, greaterThan(0.0)); expect( previousPageTwoDY, greaterThan( tester .getTopLeft( find.ancestor(of: find.text('Page 2'), matching: find.byType(CupertinoPageScaffold)), ) .dy, ), ); }); testWidgets('by default showCupertinoSheet does not enable nested navigation', ( WidgetTester tester, ) async { final GlobalKey scaffoldKey = GlobalKey(); Widget sheetScaffoldContent(BuildContext context) { return Column( children: [ const Text('Page 2'), CupertinoButton( onPressed: () { Navigator.push( context, CupertinoPageRoute( builder: (BuildContext context) { return CupertinoPageScaffold( child: Column( children: [ const Text('Page 3'), CupertinoButton(onPressed: () {}, child: const Text('Pop Page 3')), ], ), ); }, ), ); }, child: const Text('Push Page 3'), ), ], ); } await tester.pumpWidget( CupertinoApp( home: CupertinoPageScaffold( key: scaffoldKey, child: Center( child: Column( children: [ const Text('Page 1'), CupertinoButton( onPressed: () { showCupertinoSheet( context: scaffoldKey.currentContext!, pageBuilder: (BuildContext context) { return CupertinoPageScaffold(child: sheetScaffoldContent(context)); }, ); }, child: const Text('Push Page 2'), ), ], ), ), ), ), ); expect(find.text('Page 1'), findsOneWidget); await tester.tap(find.text('Push Page 2')); await tester.pumpAndSettle(); expect(find.text('Page 2'), findsOneWidget); expect(find.text('Page 3'), findsNothing); expect( tester .getTopLeft( find.ancestor(of: find.text('Page 2'), matching: find.byType(CupertinoPageScaffold)), ) .dy, greaterThan(0.0), ); await tester.tap(find.text('Push Page 3')); await tester.pumpAndSettle(); expect(find.text('Page 2'), findsNothing); expect(find.text('Page 3'), findsOneWidget); // New route should be at the top of the screen. expect( tester .getTopLeft( find.ancestor(of: find.text('Page 3'), matching: find.byType(CupertinoPageScaffold)), ) .dy, equals(0.0), ); }); testWidgets('useNestedNavigation set to true enables nested navigation', ( WidgetTester tester, ) async { final GlobalKey scaffoldKey = GlobalKey(); Widget sheetScaffoldContent(BuildContext context) { return Column( children: [ const Text('Page 2'), CupertinoButton( onPressed: () { Navigator.push( context, CupertinoPageRoute( builder: (BuildContext context) { return CupertinoPageScaffold( child: Column( children: [ const Text('Page 3'), CupertinoButton(onPressed: () {}, child: const Text('Pop Page 3')), ], ), ); }, ), ); }, child: const Text('Push Page 3'), ), ], ); } await tester.pumpWidget( CupertinoApp( home: CupertinoPageScaffold( key: scaffoldKey, child: Center( child: Column( children: [ const Text('Page 1'), CupertinoButton( onPressed: () { showCupertinoSheet( context: scaffoldKey.currentContext!, useNestedNavigation: true, pageBuilder: (BuildContext context) { return CupertinoPageScaffold(child: sheetScaffoldContent(context)); }, ); }, child: const Text('Push Page 2'), ), ], ), ), ), ), ); expect(find.text('Page 1'), findsOneWidget); await tester.tap(find.text('Push Page 2')); await tester.pumpAndSettle(); expect(find.text('Page 2'), findsOneWidget); expect(find.text('Page 3'), findsNothing); final double pageTwoDY = tester .getTopLeft( find.ancestor(of: find.text('Page 2'), matching: find.byType(CupertinoPageScaffold)), ) .dy; expect(pageTwoDY, greaterThan(0.0)); await tester.tap(find.text('Push Page 3')); await tester.pumpAndSettle(); expect(find.text('Page 2'), findsNothing); expect(find.text('Page 3'), findsOneWidget); // New route should be at the same height as the previous route. final double pageThreeDY = tester .getTopLeft( find.ancestor(of: find.text('Page 3'), matching: find.byType(CupertinoPageScaffold)), ) .dy; expect(pageThreeDY, greaterThan(0.0)); expect(pageThreeDY, equals(pageTwoDY)); }); testWidgets('useNestedNavigation handles programmatic pops', (WidgetTester tester) async { final GlobalKey scaffoldKey = GlobalKey(); Widget sheetScaffoldContent(BuildContext context) { return Column( children: [ const Text('Page 2'), CupertinoButton( onPressed: () => Navigator.of(context).maybePop(), child: const Text('Go Back'), ), ], ); } await tester.pumpWidget( CupertinoApp( home: CupertinoPageScaffold( key: scaffoldKey, child: Center( child: Column( children: [ const Text('Page 1'), CupertinoButton( onPressed: () { showCupertinoSheet( context: scaffoldKey.currentContext!, useNestedNavigation: true, pageBuilder: (BuildContext context) { return CupertinoPageScaffold(child: sheetScaffoldContent(context)); }, ); }, child: const Text('Push Page 2'), ), ], ), ), ), ), ); expect(find.text('Page 1'), findsOneWidget); // The first page is at the top of the screen. expect( tester .getTopLeft( find.ancestor(of: find.text('Page 1'), matching: find.byType(CupertinoPageScaffold)), ) .dy, equals(0.0), ); expect(find.text('Page 2'), findsNothing); await tester.tap(find.text('Push Page 2')); await tester.pumpAndSettle(); expect(find.text('Page 2'), findsOneWidget); // The first page, which is behind the top sheet but still partially visibile, is moved downwards. expect( tester .getTopLeft( find.ancestor(of: find.text('Page 1'), matching: find.byType(CupertinoPageScaffold)), ) .dy, greaterThan(0.0), ); await tester.tap(find.text('Go Back')); await tester.pumpAndSettle(); // The first page would correctly transition back and sit at the top of the screen. expect(find.text('Page 1'), findsOneWidget); expect( tester .getTopLeft( find.ancestor(of: find.text('Page 1'), matching: find.byType(CupertinoPageScaffold)), ) .dy, equals(0.0), ); expect(find.text('Page 2'), findsNothing); }); testWidgets('useNestedNavigation handles system pop gestures', (WidgetTester tester) async { final GlobalKey scaffoldKey = GlobalKey(); Widget sheetScaffoldContent(BuildContext context) { return Column( children: [ const Text('Page 2'), CupertinoButton( onPressed: () { Navigator.of(context).push( CupertinoPageRoute( builder: (BuildContext context) { return CupertinoPageScaffold( child: Column( children: [ const Text('Page 3'), CupertinoButton( onPressed: () { Navigator.of(context).pop(); }, child: const Text('Go back'), ), ], ), ); }, ), ); }, child: const Text('Push Page 3'), ), ], ); } await tester.pumpWidget( CupertinoApp( home: CupertinoPageScaffold( key: scaffoldKey, child: Center( child: Column( children: [ const Text('Page 1'), CupertinoButton( onPressed: () { showCupertinoSheet( context: scaffoldKey.currentContext!, useNestedNavigation: true, pageBuilder: (BuildContext context) { return CupertinoPageScaffold(child: sheetScaffoldContent(context)); }, ); }, child: const Text('Push Page 2'), ), ], ), ), ), ), ); expect(find.text('Page 1'), findsOneWidget); // The first page is at the top of the screen. expect( tester .getTopLeft( find.ancestor(of: find.text('Page 1'), matching: find.byType(CupertinoPageScaffold)), ) .dy, equals(0.0), ); expect(find.text('Page 2'), findsNothing); expect(find.text('Page 3'), findsNothing); await tester.tap(find.text('Push Page 2')); await tester.pumpAndSettle(); expect(find.text('Page 2'), findsOneWidget); expect(find.text('Page 3'), findsNothing); // The first page, which is behind the top sheet but still partially visibile, is moved downwards. expect( tester .getTopLeft( find.ancestor(of: find.text('Page 1'), matching: find.byType(CupertinoPageScaffold)), ) .dy, greaterThan(0.0), ); await tester.tap(find.text('Push Page 3')); await tester.pumpAndSettle(); expect(find.text('Page 3'), findsOneWidget); // Simulate a system back gesture. await simulateSystemBack(); await tester.pumpAndSettle(); // Go back to the first page within the sheet. expect(find.text('Page 2'), findsOneWidget); expect(find.text('Page 3'), findsNothing); // The first page is still stacked behind the sheet. expect( tester .getTopLeft( find.ancestor(of: find.text('Page 1'), matching: find.byType(CupertinoPageScaffold)), ) .dy, greaterThan(0.0), ); await simulateSystemBack(); await tester.pumpAndSettle(); // The first page would correctly transition back and sit at the top of the screen. expect(find.text('Page 1'), findsOneWidget); expect( tester .getTopLeft( find.ancestor(of: find.text('Page 1'), matching: find.byType(CupertinoPageScaffold)), ) .dy, equals(0.0), ); expect(find.text('Page 2'), findsNothing); }); testWidgets('sheet has route settings', (WidgetTester tester) async { await tester.pumpWidget( CupertinoApp( initialRoute: '/', onGenerateRoute: (RouteSettings settings) { if (settings.name == '/') { return PageRouteBuilder( pageBuilder: ( BuildContext context, Animation animation, Animation secondaryAnimation, ) { return CupertinoPageScaffold( navigationBar: const CupertinoNavigationBar(middle: Text('Page 1')), child: Container(), ); }, ); } return CupertinoSheetRoute( builder: (BuildContext context) { return CupertinoPageScaffold( navigationBar: CupertinoNavigationBar(middle: Text('Page: ${settings.name}')), child: Container(), ); }, ); }, ), ); expect(find.text('Page 1'), findsOneWidget); expect(find.text('Page 2'), findsNothing); tester.state(find.byType(Navigator)).pushNamed('/next'); await tester.pumpAndSettle(); expect(find.text('Page: /next'), findsOneWidget); }); testWidgets('content does not go below the bottom of the screen', (WidgetTester tester) async { final GlobalKey scaffoldKey = GlobalKey(); await tester.pumpWidget( CupertinoApp( home: CupertinoPageScaffold( key: scaffoldKey, child: Center( child: Column( children: [ const Text('Page 1'), CupertinoButton( onPressed: () { Navigator.push( scaffoldKey.currentContext!, CupertinoSheetRoute( builder: (BuildContext context) { return CupertinoPageScaffold(child: Container()); }, ), ); }, child: const Text('Push Page 2'), ), ], ), ), ), ), ); await tester.tap(find.text('Push Page 2')); await tester.pumpAndSettle(); expect(tester.getSize(find.byType(Container)).height, 600.0 - (600.0 * _kTopGapRatio)); }); testWidgets('nested navbars remove MediaQuery top padding', (WidgetTester tester) async { final GlobalKey scaffoldKey = GlobalKey(); final GlobalKey appBarKey = GlobalKey(); final GlobalKey sheetBarKey = GlobalKey(); await tester.pumpWidget( CupertinoApp( home: MediaQuery( data: const MediaQueryData(padding: EdgeInsets.fromLTRB(0, 20, 0, 0)), child: CupertinoPageScaffold( key: scaffoldKey, navigationBar: CupertinoNavigationBar( key: appBarKey, middle: const Text('Navbar'), backgroundColor: const Color(0xFFF8F8F8), ), child: Center( child: Column( children: [ const Text('Page 1'), CupertinoButton( onPressed: () { Navigator.push( scaffoldKey.currentContext!, CupertinoSheetRoute( builder: (BuildContext context) { return CupertinoPageScaffold( navigationBar: CupertinoNavigationBar( key: sheetBarKey, middle: const Text('Navbar'), ), child: Container(), ); }, ), ); }, child: const Text('Push Page 2'), ), ], ), ), ), ), ), ); final double homeNavBardHeight = tester.getSize(find.byKey(appBarKey)).height; await tester.tap(find.text('Push Page 2')); await tester.pumpAndSettle(); final double sheetNavBarHeight = tester.getSize(find.byKey(sheetBarKey)).height; expect(sheetNavBarHeight, lessThan(homeNavBardHeight)); }); testWidgets('Previous route corner radius goes to same when sheet route is popped', ( WidgetTester tester, ) async { final GlobalKey scaffoldKey = GlobalKey(); await tester.pumpWidget( CupertinoApp( home: CupertinoPageScaffold( key: scaffoldKey, child: Column( children: [ const Text('Page 1'), CupertinoButton( onPressed: () { Navigator.push( scaffoldKey.currentContext!, CupertinoSheetRoute( builder: (BuildContext context) { return CupertinoPageScaffold( child: GestureDetector( onTap: () => Navigator.pop(context), child: const Icon(Icons.arrow_back_ios), ), ); }, ), ); }, child: const Text('Push Page 2'), ), ], ), ), ), ); expect(find.text('Page 1'), findsOneWidget); expect( tester .getTopLeft( find.ancestor(of: find.text('Page 1'), matching: find.byType(CupertinoPageScaffold)), ) .dy, equals(0.0), ); expect(find.byType(Icon), findsNothing); await tester.tap(find.text('Push Page 2')); await tester.pumpAndSettle(); // Previous page is still visible behind the new sheet. expect(find.text('Page 1'), findsOneWidget); final Offset pageOneOffset = tester.getTopLeft( find.ancestor(of: find.text('Page 1'), matching: find.byType(CupertinoPageScaffold)), ); expect(pageOneOffset.dy, greaterThan(0.0)); expect(pageOneOffset.dx, greaterThan(0.0)); expect(find.byType(Icon), findsOneWidget); // Pop Sheet Route await tester.tap(find.byType(Icon)); await tester.pumpAndSettle(); expect(find.byType(ClipRSuperellipse), findsNothing); expect(find.byType(ClipRRect), findsNothing); }); testWidgets('Sheet transition does not interfere after popping', (WidgetTester tester) async { final GlobalKey homeKey = GlobalKey(); final GlobalKey sheetKey = GlobalKey(); final GlobalKey popupMenuButtonKey = GlobalKey(); await tester.pumpWidget( CupertinoApp( localizationsDelegates: const >[ GlobalMaterialLocalizations.delegate, GlobalWidgetsLocalizations.delegate, GlobalCupertinoLocalizations.delegate, ], home: CupertinoPageScaffold( key: homeKey, child: CupertinoListTile( onTap: () { showCupertinoSheet( context: homeKey.currentContext!, pageBuilder: (BuildContext context) { return CupertinoPageScaffold( key: sheetKey, child: const Center(child: Text('Page 2')), ); }, ); }, title: const Text('ListItem 0'), trailing: Material( type: MaterialType.transparency, child: PopupMenuButton( key: popupMenuButtonKey, itemBuilder: (BuildContext context) { return >[ const PopupMenuItem(child: Text('Item 0')), const PopupMenuItem(child: Text('Item 1')), ]; }, ), ), ), ), ), ); await tester.tap(find.text('ListItem 0')); await tester.pumpAndSettle(); expect(find.text('Page 2'), findsOneWidget); final TestGesture gesture = await tester.startGesture(const Offset(100, 200)); await gesture.moveBy(const Offset(0, 350)); await tester.pump(); await gesture.up(); await tester.pumpAndSettle(); expect(find.text('Page 2'), findsNothing); expect(find.text('ListItem 0'), findsOneWidget); await tester.tap(find.byKey(popupMenuButtonKey)); await tester.pumpAndSettle(); expect(find.text('Item 0'), findsOneWidget); expect(tester.takeException(), isNull); }); group('drag dismiss gesture', () { Widget dragGestureApp(GlobalKey homeScaffoldKey, GlobalKey sheetScaffoldKey) { return CupertinoApp( home: CupertinoPageScaffold( key: homeScaffoldKey, child: Center( child: Column( children: [ const Text('Page 1'), CupertinoButton( onPressed: () { showCupertinoSheet( context: homeScaffoldKey.currentContext!, pageBuilder: (BuildContext context) { return CupertinoPageScaffold( key: sheetScaffoldKey, child: const Center(child: Text('Page 2')), ); }, ); }, child: const Text('Push Page 2'), ), ], ), ), ), ); } testWidgets('partial drag and drop does not pop the sheet', (WidgetTester tester) async { final GlobalKey homeKey = GlobalKey(); final GlobalKey sheetKey = GlobalKey(); await tester.pumpWidget(dragGestureApp(homeKey, sheetKey)); await tester.tap(find.text('Push Page 2')); await tester.pumpAndSettle(); expect(find.text('Page 2'), findsOneWidget); var box = tester.renderObject(find.byKey(sheetKey)) as RenderBox; final double initialPosition = box.localToGlobal(Offset.zero).dy; final TestGesture gesture = await tester.startGesture(const Offset(100, 200)); // Partial drag down await gesture.moveBy(const Offset(0, 200)); await tester.pump(); box = tester.renderObject(find.byKey(sheetKey)) as RenderBox; final double middlePosition = box.localToGlobal(Offset.zero).dy; expect(middlePosition, greaterThan(initialPosition)); // Release gesture. Sheet should not pop and slide back up. await gesture.up(); await tester.pumpAndSettle(); expect(find.text('Page 2'), findsOneWidget); box = tester.renderObject(find.byKey(sheetKey)) as RenderBox; final double finalPosition = box.localToGlobal(Offset.zero).dy; expect(finalPosition, lessThan(middlePosition)); expect(finalPosition, equals(initialPosition)); }); testWidgets('dropping the drag further down the page pops the sheet', ( WidgetTester tester, ) async { final GlobalKey homeKey = GlobalKey(); final GlobalKey sheetKey = GlobalKey(); await tester.pumpWidget(dragGestureApp(homeKey, sheetKey)); await tester.tap(find.text('Push Page 2')); await tester.pumpAndSettle(); expect(find.text('Page 2'), findsOneWidget); final TestGesture gesture = await tester.startGesture(const Offset(100, 200)); await gesture.moveBy(const Offset(0, 350)); await tester.pump(); await gesture.up(); await tester.pumpAndSettle(); expect(find.text('Page 2'), findsNothing); }); testWidgets('dismissing with a drag pops all nested routes', (WidgetTester tester) async { final GlobalKey homeKey = GlobalKey(); final GlobalKey sheetKey = GlobalKey(); Widget sheetScaffoldContent(BuildContext context) { return Column( children: [ const Text('Page 2'), CupertinoButton( onPressed: () { Navigator.of(context).push( CupertinoPageRoute( builder: (BuildContext context) { return const CupertinoPageScaffold(child: Center(child: Text('Page 3'))); }, ), ); }, child: const Text('Push Page 3'), ), ], ); } await tester.pumpWidget( CupertinoApp( home: CupertinoPageScaffold( key: homeKey, child: Center( child: Column( children: [ const Text('Page 1'), CupertinoButton( onPressed: () { showCupertinoSheet( context: homeKey.currentContext!, useNestedNavigation: true, pageBuilder: (BuildContext context) { return CupertinoPageScaffold( key: sheetKey, child: sheetScaffoldContent(context), ); }, ); }, child: const Text('Push Page 2'), ), ], ), ), ), ), ); await tester.tap(find.text('Push Page 2')); await tester.pumpAndSettle(); expect(find.text('Page 2'), findsOneWidget); await tester.tap(find.text('Push Page 3')); await tester.pumpAndSettle(); expect(find.text('Page 3'), findsOneWidget); final TestGesture gesture = await tester.startGesture(const Offset(100, 200)); await gesture.moveBy(const Offset(0, 350)); await tester.pump(); await gesture.up(); await tester.pumpAndSettle(); expect(find.text('Page 2'), findsNothing); expect(find.text('Page 3'), findsNothing); }); testWidgets('Popping the sheet during drag should not crash', (WidgetTester tester) async { final GlobalKey homeKey = GlobalKey(); final GlobalKey sheetKey = GlobalKey(); await tester.pumpWidget(dragGestureApp(homeKey, sheetKey)); await tester.tap(find.text('Push Page 2')); await tester.pumpAndSettle(); final TestGesture gesture = await tester.createGesture(); await gesture.down(const Offset(100, 200)); // Need 2 events to form a valid drag await tester.pump(const Duration(milliseconds: 100)); await gesture.moveTo(const Offset(100, 300), timeStamp: const Duration(milliseconds: 100)); await tester.pump(const Duration(milliseconds: 200)); await gesture.moveTo(const Offset(100, 500), timeStamp: const Duration(milliseconds: 200)); Navigator.of(homeKey.currentContext!).pop(); await tester.pumpAndSettle(); expect(find.text('Page 1'), findsOneWidget); expect(find.text('Page 2'), findsNothing); await gesture.up(); await tester.pumpAndSettle(); expect(find.text('Page 1'), findsOneWidget); }); testWidgets('Sheet should not block nested scroll', (WidgetTester tester) async { final GlobalKey homeKey = GlobalKey(); Widget sheetScaffoldContent(BuildContext context) { return ListView( children: const [ Text('Top of Scroll'), SizedBox(width: double.infinity, height: 100), Text('Middle of Scroll'), SizedBox(width: double.infinity, height: 100), ], ); } await tester.pumpWidget( CupertinoApp( home: CupertinoPageScaffold( key: homeKey, child: Center( child: Column( children: [ const Text('Page 1'), CupertinoButton( onPressed: () { showCupertinoSheet( context: homeKey.currentContext!, pageBuilder: (BuildContext context) { return CupertinoPageScaffold(child: sheetScaffoldContent(context)); }, ); }, child: const Text('Push Page 2'), ), ], ), ), ), ), ); await tester.tap(find.text('Push Page 2')); await tester.pumpAndSettle(); expect(find.text('Top of Scroll'), findsOneWidget); final double startPosition = tester.getTopLeft(find.text('Middle of Scroll')).dy; final TestGesture gesture = await tester.createGesture(); await gesture.down(const Offset(100, 100)); // Need 2 events to form a valid drag. await tester.pump(const Duration(milliseconds: 100)); await gesture.moveTo(const Offset(100, 80), timeStamp: const Duration(milliseconds: 100)); await tester.pump(const Duration(milliseconds: 200)); await gesture.moveTo(const Offset(100, 50), timeStamp: const Duration(milliseconds: 200)); await tester.pumpAndSettle(); final double endPosition = tester.getTopLeft(find.text('Middle of Scroll')).dy; // Final position should be higher. expect(endPosition, lessThan(startPosition)); await gesture.up(); await tester.pumpAndSettle(); }); testWidgets('drag dismiss uses route navigator instead of root navigator', ( WidgetTester tester, ) async { final GlobalKey homeKey = GlobalKey(); final GlobalKey nestedNavigatorKey = GlobalKey(); final GlobalKey sheetKey = GlobalKey(); var wasPopped = false; var rootNavigatorPopped = false; await tester.pumpWidget( CupertinoApp( home: PopScope( onPopInvokedWithResult: (bool didPop, Object? result) { if (didPop) { rootNavigatorPopped = true; } }, child: CupertinoPageScaffold( key: homeKey, child: Navigator( key: nestedNavigatorKey, onGenerateRoute: (RouteSettings settings) { return CupertinoPageRoute( settings: settings, builder: (BuildContext context) { return Center( child: Column( children: [ const Text('Page 1'), CupertinoButton( onPressed: () { Navigator.push( context, CupertinoSheetRoute( builder: (BuildContext context) { return PopScope( onPopInvokedWithResult: (bool didPop, Object? result) { if (didPop) { wasPopped = true; } }, child: CupertinoPageScaffold( key: sheetKey, child: const Center(child: Text('Page 2')), ), ); }, ), ); }, child: const Text('Push Page 2'), ), ], ), ); }, ); }, ), ), ), ), ); await tester.tap(find.text('Push Page 2')); await tester.pumpAndSettle(); expect(find.text('Page 2'), findsOneWidget); expect(wasPopped, false); expect(rootNavigatorPopped, false); // Start drag gesture and drag down far enough to trigger dismissal final TestGesture gesture = await tester.startGesture(const Offset(100, 200)); await gesture.moveBy(const Offset(0, 350)); await tester.pump(); await gesture.up(); await tester.pumpAndSettle(); // Verify the sheet was dismissed and the PopScope callback was triggered expect(find.text('Page 2'), findsNothing); expect(find.text('Page 1'), findsOneWidget); // Verify that the nested navigator was used (sheet PopScope triggered) // but the root navigator was NOT used (root PopScope not triggered) expect(wasPopped, true); expect(rootNavigatorPopped, false); }); testWidgets('dragging does not move the sheet when enableDrag is false', ( WidgetTester tester, ) async { Widget nonDragGestureApp(GlobalKey homeScaffoldKey, GlobalKey sheetScaffoldKey) { return CupertinoApp( home: CupertinoPageScaffold( key: homeScaffoldKey, child: Center( child: Column( children: [ const Text('Page 1'), CupertinoButton( onPressed: () { showCupertinoSheet( context: homeScaffoldKey.currentContext!, pageBuilder: (BuildContext context) { return CupertinoPageScaffold( key: sheetScaffoldKey, child: const Center(child: Text('Page 2')), ); }, enableDrag: false, ); }, child: const Text('Push Page 2'), ), ], ), ), ), ); } final GlobalKey homeKey = GlobalKey(); final GlobalKey sheetKey = GlobalKey(); await tester.pumpWidget(nonDragGestureApp(homeKey, sheetKey)); await tester.tap(find.text('Push Page 2')); await tester.pumpAndSettle(); expect(find.text('Page 2'), findsOneWidget); var box = tester.renderObject(find.byKey(sheetKey)) as RenderBox; final double initialPosition = box.localToGlobal(Offset.zero).dy; final TestGesture gesture = await tester.startGesture(const Offset(100, 200)); // Partial drag down await gesture.moveBy(const Offset(0, 200)); await tester.pump(); // Release gesture. Sheet should not move. box = tester.renderObject(find.byKey(sheetKey)) as RenderBox; final double middlePosition = box.localToGlobal(Offset.zero).dy; expect(middlePosition, equals(initialPosition)); await gesture.up(); await tester.pumpAndSettle(); expect(find.text('Page 2'), findsOneWidget); box = tester.renderObject(find.byKey(sheetKey)) as RenderBox; final double finalPosition = box.localToGlobal(Offset.zero).dy; expect(finalPosition, equals(middlePosition)); expect(finalPosition, equals(initialPosition)); }); // Regression test for https://github.com/flutter/flutter/issues/163572. testWidgets('showCupertinoSheet shows snackbar at bottom of screen', ( WidgetTester tester, ) async { final scaffoldKey = GlobalKey(); void showSheet(BuildContext context) { showCupertinoSheet( context: context, pageBuilder: (BuildContext context) { return Scaffold( body: Column( children: [ const Text('Cupertino Sheet'), CupertinoButton( onPressed: () { scaffoldKey.currentState?.showSnackBar( const SnackBar(content: Text('SnackBar'), backgroundColor: Colors.red), ); }, child: const Text('Show SnackBar'), ), ], ), ); }, ); } await tester.pumpWidget( MaterialApp( scaffoldMessengerKey: scaffoldKey, home: Scaffold( body: Center( child: Column( children: [ const Text('Page 1'), Builder( builder: (BuildContext context) { return CupertinoButton( onPressed: () { showSheet(context); }, child: const Text('Show Cupertino Sheet'), ); }, ), ], ), ), ), ), ); expect(find.text('Page 1'), findsOneWidget); await tester.tap(find.text('Show Cupertino Sheet')); await tester.pumpAndSettle(); expect( tester .getTopLeft( find.ancestor(of: find.text('Cupertino Sheet'), matching: find.byType(Scaffold)), ) .dy, greaterThan(0.0), ); await tester.tap(find.text('Show SnackBar')); await tester.pumpAndSettle(); expect(find.byType(SnackBar), findsAtLeast(1)); expect( tester.getBottomLeft(find.byType(Scaffold).first).dy, equals(tester.getBottomLeft(find.byType(SnackBar).first).dy), ); final TestGesture gesture = await tester.startGesture(const Offset(200, 400)); await tester.pump(); expect( tester.getBottomLeft(find.byType(Scaffold).first).dy, equals(tester.getBottomLeft(find.byType(SnackBar).first).dy), ); await gesture.up(); await tester.pumpAndSettle(); expect( tester.getBottomLeft(find.byType(Scaffold).first).dy, equals(tester.getBottomLeft(find.byType(SnackBar).first).dy), ); }); testWidgets('partial upward drag stretches and returns without popping', ( WidgetTester tester, ) async { final GlobalKey homeKey = GlobalKey(); final GlobalKey sheetKey = GlobalKey(); await tester.pumpWidget(dragGestureApp(homeKey, sheetKey)); await tester.tap(find.text('Push Page 2')); await tester.pumpAndSettle(); expect(find.text('Page 2'), findsOneWidget); var box = tester.renderObject(find.byKey(sheetKey)) as RenderBox; final double initialPosition = box.localToGlobal(Offset.zero).dy; final TestGesture gesture = await tester.startGesture(const Offset(100, 400)); await gesture.moveBy(const Offset(0, -100)); await tester.pump(); box = tester.renderObject(find.byKey(sheetKey)) as RenderBox; final double stretchedPosition = box.localToGlobal(Offset.zero).dy; expect(stretchedPosition, lessThan(initialPosition)); await gesture.up(); await tester.pumpAndSettle(); box = tester.renderObject(find.byKey(sheetKey)) as RenderBox; final double finalPosition = box.localToGlobal(Offset.zero).dy; expect(finalPosition, initialPosition); }); }); testWidgets('CupertinoSheet causes SystemUiOverlayStyle changes', (WidgetTester tester) async { final GlobalKey scaffoldKey = GlobalKey(); await tester.pumpWidget( CupertinoApp( home: CupertinoPageScaffold( key: scaffoldKey, navigationBar: const CupertinoNavigationBar(middle: Text('SystemUiOverlayStyle')), child: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Text('Page 1'), CupertinoButton( onPressed: () { Navigator.push( scaffoldKey.currentContext!, CupertinoSheetRoute( builder: (BuildContext context) { return const CupertinoPageScaffold(child: Text('Page 2')); }, ), ); }, child: const Text('Push Page 2'), ), ], ), ), ), ), ); expect(SystemChrome.latestStyle!.statusBarBrightness, Brightness.light); expect(SystemChrome.latestStyle!.statusBarIconBrightness, Brightness.dark); await tester.tap(find.text('Push Page 2')); await tester.pumpAndSettle(); expect(SystemChrome.latestStyle!.statusBarBrightness, Brightness.dark); expect(SystemChrome.latestStyle!.statusBarIconBrightness, Brightness.light); // Returning to the previous page reverts the system UI. Navigator.of(scaffoldKey.currentContext!).pop(); await tester.pumpAndSettle(); expect(SystemChrome.latestStyle!.statusBarBrightness, Brightness.light); expect(SystemChrome.latestStyle!.statusBarIconBrightness, Brightness.dark); }); testWidgets( 'content placed in safe area of showCupertinoSheet is rendered within the safe area bounds', (WidgetTester tester) async { final GlobalKey scaffoldKey = GlobalKey(); Widget sheetScaffoldContent(BuildContext context) { return const SafeArea( child: Column( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ SizedBox(height: 80, width: double.infinity, child: Text('Top container')), SizedBox(height: 80, width: double.infinity, child: Text('Bottom container')), ], ), ); } const double bottomPadding = 50; await tester.pumpWidget( Builder( builder: (BuildContext context) { return MediaQuery( data: MediaQuery.of(context).copyWith( padding: const EdgeInsets.fromLTRB(0, 20, 0, bottomPadding), viewPadding: const EdgeInsets.fromLTRB(0, 20, 0, bottomPadding), ), child: CupertinoApp( home: CupertinoPageScaffold( key: scaffoldKey, child: Center( child: Column( children: [ const Text('Page 1'), CupertinoButton( onPressed: () { showCupertinoSheet( context: scaffoldKey.currentContext!, pageBuilder: (BuildContext context) { return CupertinoPageScaffold(child: sheetScaffoldContent(context)); }, ); }, child: const Text('Push Page 2'), ), ], ), ), ), ), ); }, ), ); expect(find.text('Page 1'), findsOneWidget); await tester.tap(find.text('Push Page 2')); await tester.pumpAndSettle(); final double pageHeight = tester .getRect( find.ancestor( of: find.text('Top container'), matching: find.byType(CupertinoPageScaffold), ), ) .bottom; expect( pageHeight - tester .getBottomLeft( find .ancestor(of: find.text('Bottom container'), matching: find.byType(SizedBox)) .first, ) .dy, bottomPadding, ); }, ); group('topGap parameter tests', () { testWidgets('sheet uses default topGap when not specified', (WidgetTester tester) async { final GlobalKey scaffoldKey = GlobalKey(); await tester.pumpWidget( CupertinoApp( home: CupertinoPageScaffold( key: scaffoldKey, child: Center( child: Column( children: [ const Text('Page 1'), CupertinoButton( onPressed: () { Navigator.push( scaffoldKey.currentContext!, CupertinoSheetRoute( builder: (BuildContext context) { return const CupertinoPageScaffold(child: Text('Page 2')); }, ), ); }, child: const Text('Push Page 2'), ), ], ), ), ), ), ); await tester.tap(find.text('Push Page 2')); await tester.pumpAndSettle(); expect(find.text('Page 2'), findsOneWidget); final double sheetTopOffset = tester .getTopLeft( find.ancestor(of: find.text('Page 2'), matching: find.byType(CupertinoPageScaffold)), ) .dy; // Should use default topGap ratio (8% of screen height = 0.08 * 600.0 = 48.0) expect(sheetTopOffset, equals(600.0 * _kTopGapRatio)); }); testWidgets('sheet with custom topGap uses custom positioning', (WidgetTester tester) async { final GlobalKey scaffoldKey = GlobalKey(); await tester.pumpWidget( CupertinoApp( home: CupertinoPageScaffold( key: scaffoldKey, child: Center( child: Column( children: [ const Text('Page 1'), CupertinoButton( onPressed: () { Navigator.push( scaffoldKey.currentContext!, CupertinoSheetRoute( builder: (BuildContext context) { return const CupertinoPageScaffold(child: Text('Page 2')); }, topGap: 0.0, ), ); }, child: const Text('Push Page 2'), ), ], ), ), ), ), ); await tester.tap(find.text('Push Page 2')); await tester.pumpAndSettle(); expect(find.text('Page 2'), findsOneWidget); final double sheetTopOffset = tester .getTopLeft( find.ancestor(of: find.text('Page 2'), matching: find.byType(CupertinoPageScaffold)), ) .dy; expect(sheetTopOffset, equals(0.0)); }); testWidgets('showCupertinoSheet accepts topGap parameter', (WidgetTester tester) async { final GlobalKey scaffoldKey = GlobalKey(); await tester.pumpWidget( CupertinoApp( home: CupertinoPageScaffold( key: scaffoldKey, child: Center( child: Column( children: [ const Text('Page 1'), CupertinoButton( onPressed: () { showCupertinoSheet( context: scaffoldKey.currentContext!, topGap: 0.15, builder: (BuildContext context) { return const CupertinoPageScaffold(child: Text('Page 2')); }, ); }, child: const Text('Push Page 2'), ), ], ), ), ), ), ); await tester.tap(find.text('Push Page 2')); await tester.pumpAndSettle(); expect(find.text('Page 2'), findsOneWidget); final double sheetTopOffset = tester .getTopLeft( find.ancestor(of: find.text('Page 2'), matching: find.byType(CupertinoPageScaffold)), ) .dy; expect(sheetTopOffset, equals(600.0 * 0.15)); }); testWidgets('custom topGap disables delegated transitions', (WidgetTester tester) async { final GlobalKey scaffoldKey = GlobalKey(); await tester.pumpWidget( CupertinoApp( home: CupertinoPageScaffold( key: scaffoldKey, child: Column( children: [ const Text('Page 1'), CupertinoButton( onPressed: () { Navigator.push( scaffoldKey.currentContext!, CupertinoSheetRoute( builder: (BuildContext context) { return CupertinoPageScaffold( child: Column( children: [ const Text('Page 2'), CupertinoButton( onPressed: () { Navigator.push( scaffoldKey.currentContext!, CupertinoSheetRoute( builder: (BuildContext context) { return const CupertinoPageScaffold(child: Text('Page 3')); }, topGap: 0.1, // Custom topGap should disable transitions ), ); }, child: const Text('Push Page 3'), ), ], ), ); }, ), ); }, child: const Text('Push Page 2'), ), ], ), ), ), ); await tester.tap(find.text('Push Page 2')); await tester.pumpAndSettle(); final double pageTwoYBeforePage3 = tester .getTopLeft( find.ancestor(of: find.text('Page 2'), matching: find.byType(CupertinoPageScaffold)), ) .dy; await tester.tap(find.text('Push Page 3')); await tester.pumpAndSettle(); final double pageTwoYAfterPage3 = tester .getTopLeft( find.ancestor(of: find.text('Page 2'), matching: find.byType(CupertinoPageScaffold)), ) .dy; // Page 2 should remain at the same position because custom topGap disables transitions expect(pageTwoYAfterPage3, equals(pageTwoYBeforePage3)); final double pageThreeY = tester .getTopLeft( find.ancestor(of: find.text('Page 3'), matching: find.byType(CupertinoPageScaffold)), ) .dy; expect(pageThreeY, equals(600.0 * 0.1)); }); testWidgets('default topGap allows delegated transitions', (WidgetTester tester) async { final GlobalKey scaffoldKey = GlobalKey(); await tester.pumpWidget( CupertinoApp( home: CupertinoPageScaffold( key: scaffoldKey, child: Column( children: [ const Text('Page 1'), CupertinoButton( onPressed: () { Navigator.push( scaffoldKey.currentContext!, CupertinoSheetRoute( builder: (BuildContext context) { return CupertinoPageScaffold( child: Column( children: [ const Text('Page 2'), CupertinoButton( onPressed: () { Navigator.push( scaffoldKey.currentContext!, CupertinoSheetRoute( builder: (BuildContext context) { return const CupertinoPageScaffold(child: Text('Page 3')); }, // No topGap specified - should use default and allow transitions ), ); }, child: const Text('Push Page 3'), ), ], ), ); }, ), ); }, child: const Text('Push Page 2'), ), ], ), ), ), ); await tester.tap(find.text('Push Page 2')); await tester.pumpAndSettle(); final double pageTwoYBeforePage3 = tester .getTopLeft( find.ancestor(of: find.text('Page 2'), matching: find.byType(CupertinoPageScaffold)), ) .dy; await tester.tap(find.text('Push Page 3')); await tester.pumpAndSettle(); final double pageTwoYAfterPage3 = tester .getTopLeft( find.ancestor(of: find.text('Page 2'), matching: find.byType(CupertinoPageScaffold)), ) .dy; // Page 2 should move upward because default topGap allows delegated transitions expect(pageTwoYAfterPage3, lessThan(pageTwoYBeforePage3)); }); testWidgets('topGap affects drag gesture calculations', (WidgetTester tester) async { final GlobalKey scaffoldKey = GlobalKey(); Widget dragGestureAppWithTopGap(double topGap) { return CupertinoApp( home: CupertinoPageScaffold( key: scaffoldKey, child: Center( child: Column( children: [ const Text('Page 1'), CupertinoButton( onPressed: () { showCupertinoSheet( context: scaffoldKey.currentContext!, topGap: topGap, pageBuilder: (BuildContext context) { return const CupertinoPageScaffold(child: Center(child: Text('Page 2'))); }, ); }, child: const Text('Push Page 2'), ), ], ), ), ), ); } // Test with custom topGap of 0.3 await tester.pumpWidget(dragGestureAppWithTopGap(0.3)); await tester.tap(find.text('Push Page 2')); await tester.pumpAndSettle(); expect(find.text('Page 2'), findsOneWidget); final double sheetTopOffset = tester .getTopLeft( find.ancestor(of: find.text('Page 2'), matching: find.byType(CupertinoPageScaffold)), ) .dy; expect(sheetTopOffset, equals(600.0 * 0.3)); // Test that drag still works with custom topGap final TestGesture gesture = await tester.startGesture(const Offset(100, 300)); await gesture.moveBy(const Offset(0, 100)); await tester.pump(); final double draggedPosition = tester .getTopLeft( find.ancestor(of: find.text('Page 2'), matching: find.byType(CupertinoPageScaffold)), ) .dy; // Sheet should move down when dragged expect(draggedPosition, greaterThan(sheetTopOffset)); await gesture.up(); await tester.pumpAndSettle(); }); }); testWidgets('didUpdateWidget in sheet transition does not try and use multiple tickers', ( WidgetTester tester, ) async { final animation = AnimationController(vsync: const TestVSync()); final secondaryAnimation = AnimationController(vsync: const TestVSync()); await tester.pumpWidget( CupertinoSheetTransition( primaryRouteAnimation: animation, secondaryRouteAnimation: secondaryAnimation, linearTransition: false, child: const SizedBox(height: 100, width: 100), ), ); final newAnimation = AnimationController(vsync: const TestVSync()); // Should not throw an exception. await tester.pumpWidget( CupertinoSheetTransition( primaryRouteAnimation: newAnimation, secondaryRouteAnimation: secondaryAnimation, linearTransition: false, child: const SizedBox(height: 100, width: 100), ), ); await tester.pumpAndSettle(); expect(tester.takeException(), isNull); animation.dispose(); secondaryAnimation.dispose(); newAnimation.dispose(); }); }