Navaron Bracke 5b1c84cec1
[Reland] Cupertino cross imports (#182416)
This is a reland of https://github.com/flutter/flutter/pull/181634 which
broke the tree, because https://github.com/flutter/flutter/pull/182395
also landed and probably conflicted with it.

Part of https://github.com/flutter/flutter/issues/177415

*If you had to change anything in the [flutter/tests] repo, include a
link to the migration guide as per the [breaking change policy].*

## Pre-launch Checklist

- [x] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [x] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [x] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [x] I signed the [CLA].
- [x] I listed at least one issue that this PR fixes in the description
above.
- [x] I updated/added relevant documentation (doc comments with `///`).
- [x] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [x] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [x] All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel
on [Discord].

**Note**: The Flutter team is currently trialing the use of [Gemini Code
Assist for
GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code).
Comments from the `gemini-code-assist` bot should not be taken as
authoritative feedback from the Flutter team. If you find its comments
useful you can update your code accordingly, but if you are unsure or
disagree with the feedback, please feel free to wait for a Flutter team
member's review for guidance on which automated comments should be
addressed.

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
2026-02-17 19:35:11 +00:00

2276 lines
75 KiB
Dart

// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/cupertino.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: <Widget>[
const Text('Page 1'),
CupertinoButton(
onPressed: () {
Navigator.push<void>(
scaffoldKey.currentContext!,
CupertinoSheetRoute<void>(
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: <Widget>[
const Text('Page 1'),
CupertinoButton(
onPressed: () {
Navigator.push<void>(
scaffoldKey.currentContext!,
CupertinoSheetRoute<void>(
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: <Widget>[
const Text('Page 1'),
CupertinoButton(
onPressed: () {
Navigator.push<void>(
scaffoldKey.currentContext!,
CupertinoSheetRoute<void>(
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: <Widget>[
const Text('Page 1'),
CupertinoButton(
onPressed: () {
Navigator.push<void>(
scaffoldKey.currentContext!,
CupertinoSheetRoute<void>(
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: <Widget>[
const Text('Page 1'),
CupertinoButton(
onPressed: () {
Navigator.push<void>(
scaffoldKey.currentContext!,
CupertinoSheetRoute<void>(
builder: (BuildContext context) {
return CupertinoPageScaffold(
child: Column(
children: <Widget>[
const Text('Page 2'),
CupertinoButton(
onPressed: () {
Navigator.push<void>(
scaffoldKey.currentContext!,
CupertinoSheetRoute<void>(
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: <Widget>[
const Text('Page 2'),
CupertinoButton(
onPressed: () {
Navigator.push<void>(
context,
CupertinoPageRoute<void>(
builder: (BuildContext context) {
return CupertinoPageScaffold(
child: Column(
children: <Widget>[
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: <Widget>[
const Text('Page 1'),
CupertinoButton(
onPressed: () {
showCupertinoSheet<void>(
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: <Widget>[
const Text('Page 2'),
CupertinoButton(
onPressed: () {
Navigator.push<void>(
context,
CupertinoPageRoute<void>(
builder: (BuildContext context) {
return CupertinoPageScaffold(
child: Column(
children: <Widget>[
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: <Widget>[
const Text('Page 1'),
CupertinoButton(
onPressed: () {
showCupertinoSheet<void>(
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: <Widget>[
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: <Widget>[
const Text('Page 1'),
CupertinoButton(
onPressed: () {
showCupertinoSheet<void>(
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: <Widget>[
const Text('Page 2'),
CupertinoButton(
onPressed: () {
Navigator.of(context).push(
CupertinoPageRoute<void>(
builder: (BuildContext context) {
return CupertinoPageScaffold(
child: Column(
children: <Widget>[
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: <Widget>[
const Text('Page 1'),
CupertinoButton(
onPressed: () {
showCupertinoSheet<void>(
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<void>(
pageBuilder:
(
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
) {
return CupertinoPageScaffold(
navigationBar: const CupertinoNavigationBar(middle: Text('Page 1')),
child: Container(),
);
},
);
}
return CupertinoSheetRoute<void>(
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<NavigatorState>(find.byType(Navigator)).pushNamed('/next');
await tester.pumpAndSettle();
expect(find.text('Page: /next'), findsOneWidget);
});
testWidgets('sheet with RouteSettings', (WidgetTester tester) async {
late RouteSettings currentRouteSetting;
final GlobalKey scaffoldKey = GlobalKey();
await tester.pumpWidget(
CupertinoApp(
navigatorObservers: <NavigatorObserver>[
_ClosureNavigatorObserver(
onDidChange: (Route<dynamic> newRoute) {
currentRouteSetting = newRoute.settings;
},
),
],
home: CupertinoPageScaffold(
key: scaffoldKey,
child: Center(
child: Column(
children: <Widget>[
const Text('Page 1'),
CupertinoButton(
onPressed: () {
showCupertinoSheet<void>(
settings: const RouteSettings(name: 'simpleroute'),
context: scaffoldKey.currentContext!,
pageBuilder: (BuildContext context) {
return const CupertinoPageScaffold(child: Text('Hello'));
},
);
},
child: const Text('Push Page 2'),
),
],
),
),
),
),
);
expect(currentRouteSetting.name, '/');
expect(find.text('Page 1'), findsOneWidget);
expect(find.text('Hello'), findsNothing);
await tester.tap(find.text('Push Page 2'));
await tester.pumpAndSettle();
expect(find.text('Hello'), findsOneWidget);
expect(currentRouteSetting.name, 'simpleroute');
Navigator.of(scaffoldKey.currentContext!).pop();
await tester.pumpAndSettle();
expect(currentRouteSetting.name, '/');
});
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: <Widget>[
const Text('Page 1'),
CupertinoButton(
onPressed: () {
Navigator.push<void>(
scaffoldKey.currentContext!,
CupertinoSheetRoute<void>(
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: <Widget>[
const Text('Page 1'),
CupertinoButton(
onPressed: () {
Navigator.push<void>(
scaffoldKey.currentContext!,
CupertinoSheetRoute<void>(
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: <Widget>[
const Text('Page 1'),
CupertinoButton(
onPressed: () {
Navigator.push<void>(
scaffoldKey.currentContext!,
CupertinoSheetRoute<void>(
builder: (BuildContext context) {
return CupertinoPageScaffold(
child: GestureDetector(
onTap: () => Navigator.pop(context),
child: const Icon(CupertinoIcons.back),
),
);
},
),
);
},
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();
await tester.pumpWidget(
CupertinoApp(
localizationsDelegates: const <LocalizationsDelegate<dynamic>>[
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
home: CupertinoPageScaffold(
key: homeKey,
child: CupertinoListTile(
onTap: () {
showCupertinoSheet<void>(
context: homeKey.currentContext!,
pageBuilder: (BuildContext context) {
return CupertinoPageScaffold(
key: sheetKey,
child: const Center(child: Text('Page 2')),
);
},
);
},
title: const Text('ListItem 0'),
trailing: Builder(
builder: (context) {
return CupertinoContextMenu(
actions: [
CupertinoContextMenuAction(
child: const Text('Item 0'),
onPressed: () => Navigator.of(context).pop(),
),
],
child: const Text('Button'),
);
},
),
),
),
),
);
await tester.tap(find.text('ListItem 0'));
await tester.pumpAndSettle();
expect(find.text('Page 2'), findsOneWidget);
final TestGesture gestureOne = await tester.startGesture(const Offset(100, 200));
await gestureOne.moveBy(const Offset(0, 350));
await tester.pump();
await gestureOne.up();
await tester.pumpAndSettle();
expect(find.text('Page 2'), findsNothing);
expect(find.text('ListItem 0'), findsOneWidget);
final Offset contextMenuButton = tester.getCenter(find.text('Button'));
expect(find.text('Item 0'), findsNothing);
final TestGesture gestureTwo = await tester.startGesture(contextMenuButton);
await tester.pumpAndSettle();
expect(find.text('Item 0'), findsOneWidget);
expect(tester.takeException(), isNull);
await tester.tap(find.text('Item 0'));
await tester.pumpAndSettle();
await gestureTwo.up();
await tester.pumpAndSettle();
expect(find.text('Item 0'), findsNothing);
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: <Widget>[
const Text('Page 1'),
CupertinoButton(
onPressed: () {
showCupertinoSheet<void>(
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: <Widget>[
const Text('Page 2'),
CupertinoButton(
onPressed: () {
Navigator.of(context).push(
CupertinoPageRoute<void>(
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: <Widget>[
const Text('Page 1'),
CupertinoButton(
onPressed: () {
showCupertinoSheet<void>(
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 <Widget>[
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: <Widget>[
const Text('Page 1'),
CupertinoButton(
onPressed: () {
showCupertinoSheet<void>(
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<NavigatorState>();
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<void>(
settings: settings,
builder: (BuildContext context) {
return Center(
child: Column(
children: <Widget>[
const Text('Page 1'),
CupertinoButton(
onPressed: () {
Navigator.push<void>(
context,
CupertinoSheetRoute<void>(
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: <Widget>[
const Text('Page 1'),
CupertinoButton(
onPressed: () {
showCupertinoSheet<void>(
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));
});
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);
});
});
group('draggable scrollable CupertinoSheetRoute', () {
Widget draggableScrollableApp(GlobalKey homeScaffoldKey, GlobalKey sheetScaffoldKey) {
return CupertinoApp(
home: CupertinoPageScaffold(
key: homeScaffoldKey,
child: Center(
child: Column(
children: <Widget>[
const Text('Page 1'),
CupertinoButton(
onPressed: () {
showCupertinoSheet<void>(
context: homeScaffoldKey.currentContext!,
scrollableBuilder: (BuildContext context, ScrollController controller) {
return CupertinoPageScaffold(
key: sheetScaffoldKey,
child: CustomScrollView(
controller: controller,
primary: false,
slivers: <Widget>[
SliverList(
delegate: SliverChildBuilderDelegate((
BuildContext context,
int index,
) {
return Container(
alignment: Alignment.center,
height: 100,
child: Text('Scroll Item $index'),
);
}, childCount: 20),
),
],
),
);
},
);
},
child: const Text('Push Page 2'),
),
],
),
),
),
);
}
testWidgets('Can be scrolled when at full height', (WidgetTester tester) async {
final GlobalKey homeKey = GlobalKey();
final GlobalKey sheetKey = GlobalKey();
await tester.pumpWidget(draggableScrollableApp(homeKey, sheetKey));
await tester.tap(find.text('Push Page 2'));
await tester.pumpAndSettle();
var box = tester.renderObject(find.byKey(sheetKey)) as RenderBox;
final double initialSheetPosition = box.localToGlobal(Offset.zero).dy;
box = tester.renderObject(find.text('Scroll Item 3')) as RenderBox;
final double initialScrollPosition = box.localToGlobal(Offset.zero).dy;
final TestGesture gesture = await tester.startGesture(const Offset(100, 300));
// Do a small drag first to win the gesture arena.
await gesture.moveBy(const Offset(0, -30));
await gesture.moveBy(const Offset(0, -100));
await tester.pump();
box = tester.renderObject(find.byKey(sheetKey)) as RenderBox;
final double finalSheetPosition = box.localToGlobal(Offset.zero).dy;
box = tester.renderObject(find.text('Scroll Item 3')) as RenderBox;
final double finalScrollPosition = box.localToGlobal(Offset.zero).dy;
expect(finalSheetPosition, equals(initialSheetPosition));
expect(finalScrollPosition, lessThan(initialScrollPosition));
await gesture.up();
await tester.pumpAndSettle();
});
testWidgets('Sheet slides down on downwards drag when scrollable content is at the top', (
WidgetTester tester,
) async {
final GlobalKey homeKey = GlobalKey();
final GlobalKey sheetKey = GlobalKey();
await tester.pumpWidget(draggableScrollableApp(homeKey, sheetKey));
await tester.tap(find.text('Push Page 2'));
await tester.pumpAndSettle();
var box = tester.renderObject(find.byKey(sheetKey)) as RenderBox;
final double initialSheetPosition = box.localToGlobal(Offset.zero).dy;
box = tester.renderObject(find.text('Scroll Item 3')) as RenderBox;
final double initialScrollPosition = box.localToGlobal(Offset.zero).dy;
final TestGesture gesture = await tester.startGesture(const Offset(100, 300));
// Do a small drag first to win the gesture arena.
await gesture.moveBy(const Offset(0, 30));
await gesture.moveBy(const Offset(0, 100));
await tester.pump();
box = tester.renderObject(find.byKey(sheetKey)) as RenderBox;
final double finalSheetPosition = box.localToGlobal(Offset.zero).dy;
box = tester.renderObject(find.text('Scroll Item 3')) as RenderBox;
final double finalScrollPosition = box.localToGlobal(Offset.zero).dy;
expect(finalSheetPosition, greaterThan(initialSheetPosition));
// Scroll should move down with sheet.
expect(finalScrollPosition, greaterThan(initialScrollPosition));
expect(
finalScrollPosition - initialScrollPosition,
closeTo(finalSheetPosition - initialSheetPosition, 0.0005),
);
await gesture.up();
await tester.pumpAndSettle();
});
testWidgets(
'While the drag gesture continues, the sheet switches between scrolling and dismiss animation correctly',
(WidgetTester tester) async {
final GlobalKey homeKey = GlobalKey();
final GlobalKey sheetKey = GlobalKey();
await tester.pumpWidget(draggableScrollableApp(homeKey, sheetKey));
await tester.tap(find.text('Push Page 2'));
await tester.pumpAndSettle();
var box = tester.renderObject(find.byKey(sheetKey)) as RenderBox;
final double initialSheetPosition = box.localToGlobal(Offset.zero).dy;
box = tester.renderObject(find.text('Scroll Item 3')) as RenderBox;
final double initialScrollPosition = box.localToGlobal(Offset.zero).dy;
// Sheet will scroll on upwards drag.
final TestGesture gesture = await tester.startGesture(const Offset(100, 300));
// Do a small drag first to win the gesture arena.
await gesture.moveBy(const Offset(0, -30));
await gesture.moveBy(const Offset(0, -100));
await tester.pump();
box = tester.renderObject(find.byKey(sheetKey)) as RenderBox;
double currentSheetPosition = box.localToGlobal(Offset.zero).dy;
box = tester.renderObject(find.text('Scroll Item 3')) as RenderBox;
double currentScrollPosition = box.localToGlobal(Offset.zero).dy;
// Sheet has not moved, but the scroll was triggered.
expect(currentSheetPosition, equals(initialSheetPosition));
expect(currentScrollPosition, lessThan(initialScrollPosition));
// Drag back down the same amount.
await gesture.moveBy(const Offset(0, 100));
await tester.pump();
box = tester.renderObject(find.byKey(sheetKey)) as RenderBox;
currentSheetPosition = box.localToGlobal(Offset.zero).dy;
box = tester.renderObject(find.text('Scroll Item 3')) as RenderBox;
currentScrollPosition = box.localToGlobal(Offset.zero).dy;
// Sheet still has not moved, and the scroll returns to it's original spot.
expect(currentSheetPosition, equals(initialSheetPosition));
expect(currentScrollPosition, equals(initialScrollPosition));
// Drag downwards further.
await gesture.moveBy(const Offset(0, 100));
await tester.pump();
box = tester.renderObject(find.byKey(sheetKey)) as RenderBox;
currentSheetPosition = box.localToGlobal(Offset.zero).dy;
box = tester.renderObject(find.text('Scroll Item 3')) as RenderBox;
currentScrollPosition = box.localToGlobal(Offset.zero).dy;
// Entire sheet will have dragged down. Scrollable content will have moved down with it.
expect(currentSheetPosition, greaterThan(initialSheetPosition));
expect(currentScrollPosition, greaterThan(initialScrollPosition));
expect(
currentScrollPosition - initialScrollPosition,
equals(currentSheetPosition - initialSheetPosition),
);
await gesture.up();
await tester.pumpAndSettle();
},
);
testWidgets('Fling while scrolled down does not trigger drag pop', (WidgetTester tester) async {
final GlobalKey homeKey = GlobalKey();
final GlobalKey sheetKey = GlobalKey();
await tester.pumpWidget(draggableScrollableApp(homeKey, sheetKey));
await tester.tap(find.text('Push Page 2'));
await tester.pumpAndSettle();
// Scroll down some.
final TestGesture gesture = await tester.startGesture(const Offset(100, 300));
// Do a small drag first to win the gesture arena.
await gesture.moveBy(const Offset(0, -30));
await gesture.moveBy(const Offset(0, -400));
await tester.pump();
await gesture.up();
await tester.pumpAndSettle();
// Trigger fling up.
await tester.flingFrom(const Offset(100, 400), const Offset(0, 300), 500);
await tester.pumpAndSettle();
// Scrollable sheet should still be open.
expect(find.text('Scroll Item 3'), findsOneWidget);
});
testWidgets('Fling while scrolled to the top causes the drag fling', (
WidgetTester tester,
) async {
final GlobalKey homeKey = GlobalKey();
final GlobalKey sheetKey = GlobalKey();
await tester.pumpWidget(draggableScrollableApp(homeKey, sheetKey));
await tester.tap(find.text('Push Page 2'));
await tester.pumpAndSettle();
// Trigger fling up.
await tester.flingFrom(const Offset(100, 400), const Offset(0, 300), 500);
await tester.pumpAndSettle();
// Scrollable sheet should not be open.
expect(find.text('Scroll Item 3'), findsNothing);
});
});
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: <Widget>[
const Text('Page 1'),
CupertinoButton(
onPressed: () {
Navigator.push<void>(
scaffoldKey.currentContext!,
CupertinoSheetRoute<void>(
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: <Widget>[
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: <Widget>[
const Text('Page 1'),
CupertinoButton(
onPressed: () {
showCupertinoSheet<void>(
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: <Widget>[
const Text('Page 1'),
CupertinoButton(
onPressed: () {
Navigator.push<void>(
scaffoldKey.currentContext!,
CupertinoSheetRoute<void>(
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: <Widget>[
const Text('Page 1'),
CupertinoButton(
onPressed: () {
Navigator.push<void>(
scaffoldKey.currentContext!,
CupertinoSheetRoute<void>(
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: <Widget>[
const Text('Page 1'),
CupertinoButton(
onPressed: () {
showCupertinoSheet<void>(
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: <Widget>[
const Text('Page 1'),
CupertinoButton(
onPressed: () {
Navigator.push<void>(
scaffoldKey.currentContext!,
CupertinoSheetRoute<void>(
builder: (BuildContext context) {
return CupertinoPageScaffold(
child: Column(
children: <Widget>[
const Text('Page 2'),
CupertinoButton(
onPressed: () {
Navigator.push<void>(
scaffoldKey.currentContext!,
CupertinoSheetRoute<void>(
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: <Widget>[
const Text('Page 1'),
CupertinoButton(
onPressed: () {
Navigator.push<void>(
scaffoldKey.currentContext!,
CupertinoSheetRoute<void>(
builder: (BuildContext context) {
return CupertinoPageScaffold(
child: Column(
children: <Widget>[
const Text('Page 2'),
CupertinoButton(
onPressed: () {
Navigator.push<void>(
scaffoldKey.currentContext!,
CupertinoSheetRoute<void>(
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: <Widget>[
const Text('Page 1'),
CupertinoButton(
onPressed: () {
showCupertinoSheet<void>(
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();
});
}
class _ClosureNavigatorObserver extends NavigatorObserver {
_ClosureNavigatorObserver({required this.onDidChange});
final void Function(Route<dynamic> newRoute) onDidChange;
@override
void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) => onDidChange(route);
@override
void didPop(Route<dynamic> route, Route<dynamic>? previousRoute) => onDidChange(previousRoute!);
@override
void didRemove(Route<dynamic> route, Route<dynamic>? previousRoute) =>
onDidChange(previousRoute!);
@override
void didReplace({Route<dynamic>? newRoute, Route<dynamic>? oldRoute}) => onDidChange(newRoute!);
}