blob: ceb80e4427e724f48931bb5ed0538204da2db809 [file] [log] [blame]
// 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_test/flutter_test.dart';
void main() {
testWidgets('test iOS page transition (LTR)', (WidgetTester tester) async {
await tester.pumpWidget(
CupertinoApp(
onGenerateRoute: (RouteSettings settings) {
return CupertinoPageRoute<void>(
settings: settings,
builder: (BuildContext context) {
final String pageNumber = settings.name == '/' ? '1' : '2';
return Center(child: Text('Page $pageNumber'));
},
);
},
),
);
final Offset widget1InitialTopLeft = tester.getTopLeft(find.text('Page 1'));
tester.state<NavigatorState>(find.byType(Navigator)).pushNamed('/next');
await tester.pump();
await tester.pump(const Duration(milliseconds: 150));
Offset widget1TransientTopLeft = tester.getTopLeft(find.text('Page 1'));
Offset widget2TopLeft = tester.getTopLeft(find.text('Page 2'));
// Page 1 is moving to the left.
expect(widget1TransientTopLeft.dx, lessThan(widget1InitialTopLeft.dx));
// Page 1 isn't moving vertically.
expect(widget1TransientTopLeft.dy, equals(widget1InitialTopLeft.dy));
// iOS transition is horizontal only.
expect(widget1InitialTopLeft.dy, equals(widget2TopLeft.dy));
// Page 2 is coming in from the right.
expect(widget2TopLeft.dx, greaterThan(widget1InitialTopLeft.dx));
// Will need to be changed if the animation curve or duration changes.
expect(widget1TransientTopLeft.dx, moreOrLessEquals(158, epsilon: 1.0));
await tester.pumpAndSettle();
// Page 2 covers page 1.
expect(find.text('Page 1'), findsNothing);
expect(find.text('Page 2'), isOnstage);
tester.state<NavigatorState>(find.byType(Navigator)).pop();
await tester.pump();
await tester.pump(const Duration(milliseconds: 100));
widget1TransientTopLeft = tester.getTopLeft(find.text('Page 1'));
widget2TopLeft = tester.getTopLeft(find.text('Page 2'));
// Page 1 is coming back from the left.
expect(widget1TransientTopLeft.dx, lessThan(widget1InitialTopLeft.dx));
// Page 1 isn't moving vertically.
expect(widget1TransientTopLeft.dy, equals(widget1InitialTopLeft.dy));
// iOS transition is horizontal only.
expect(widget1InitialTopLeft.dy, equals(widget2TopLeft.dy));
// Page 2 is leaving towards the right.
expect(widget2TopLeft.dx, greaterThan(widget1InitialTopLeft.dx));
// Will need to be changed if the animation curve or duration changes.
expect(widget1TransientTopLeft.dx, moreOrLessEquals(220, epsilon: 1.0));
await tester.pumpAndSettle();
expect(find.text('Page 1'), isOnstage);
expect(find.text('Page 2'), findsNothing);
widget1TransientTopLeft = tester.getTopLeft(find.text('Page 1'));
// Page 1 is back where it started.
expect(widget1InitialTopLeft, equals(widget1TransientTopLeft));
});
testWidgets('test iOS page transition (RTL)', (WidgetTester tester) async {
await tester.pumpWidget(
CupertinoApp(
localizationsDelegates: const <LocalizationsDelegate<dynamic>>[
RtlOverrideWidgetsDelegate(),
],
onGenerateRoute: (RouteSettings settings) {
return CupertinoPageRoute<void>(
settings: settings,
builder: (BuildContext context) {
final String pageNumber = settings.name == '/' ? '1' : '2';
return Center(child: Text('Page $pageNumber'));
},
);
},
),
);
await tester.pump(); // to load the localization, since it doesn't use a synchronous future
final Offset widget1InitialTopLeft = tester.getTopLeft(find.text('Page 1'));
tester.state<NavigatorState>(find.byType(Navigator)).pushNamed('/next');
await tester.pump();
await tester.pump(const Duration(milliseconds: 150));
Offset widget1TransientTopLeft = tester.getTopLeft(find.text('Page 1'));
Offset widget2TopLeft = tester.getTopLeft(find.text('Page 2'));
// Page 1 is moving to the right.
expect(widget1TransientTopLeft.dx, greaterThan(widget1InitialTopLeft.dx));
// Page 1 isn't moving vertically.
expect(widget1TransientTopLeft.dy, equals(widget1InitialTopLeft.dy));
// iOS transition is horizontal only.
expect(widget1InitialTopLeft.dy, equals(widget2TopLeft.dy));
// Page 2 is coming in from the left.
expect(widget2TopLeft.dx, lessThan(widget1InitialTopLeft.dx));
await tester.pumpAndSettle();
// Page 2 covers page 1.
expect(find.text('Page 1'), findsNothing);
expect(find.text('Page 2'), isOnstage);
tester.state<NavigatorState>(find.byType(Navigator)).pop();
await tester.pump();
await tester.pump(const Duration(milliseconds: 100));
widget1TransientTopLeft = tester.getTopLeft(find.text('Page 1'));
widget2TopLeft = tester.getTopLeft(find.text('Page 2'));
// Page 1 is coming back from the right.
expect(widget1TransientTopLeft.dx, greaterThan(widget1InitialTopLeft.dx));
// Page 1 isn't moving vertically.
expect(widget1TransientTopLeft.dy, equals(widget1InitialTopLeft.dy));
// iOS transition is horizontal only.
expect(widget1InitialTopLeft.dy, equals(widget2TopLeft.dy));
// Page 2 is leaving towards the left.
expect(widget2TopLeft.dx, lessThan(widget1InitialTopLeft.dx));
await tester.pumpAndSettle();
expect(find.text('Page 1'), isOnstage);
expect(find.text('Page 2'), findsNothing);
widget1TransientTopLeft = tester.getTopLeft(find.text('Page 1'));
// Page 1 is back where it started.
expect(widget1InitialTopLeft, equals(widget1TransientTopLeft));
});
testWidgets('test iOS fullscreen dialog transition', (WidgetTester tester) async {
await tester.pumpWidget(
const CupertinoApp(
home: Center(child: Text('Page 1')),
),
);
final Offset widget1InitialTopLeft = tester.getTopLeft(find.text('Page 1'));
tester.state<NavigatorState>(find.byType(Navigator)).push(CupertinoPageRoute<void>(
builder: (BuildContext context) {
return const Center(child: Text('Page 2'));
},
fullscreenDialog: true,
));
await tester.pump();
await tester.pump(const Duration(milliseconds: 100));
Offset widget1TransientTopLeft = tester.getTopLeft(find.text('Page 1'));
Offset widget2TopLeft = tester.getTopLeft(find.text('Page 2'));
// Page 1 doesn't move.
expect(widget1TransientTopLeft, equals(widget1InitialTopLeft));
// Fullscreen dialogs transitions vertically only.
expect(widget1InitialTopLeft.dx, equals(widget2TopLeft.dx));
// Page 2 is coming in from the bottom.
expect(widget2TopLeft.dy, greaterThan(widget1InitialTopLeft.dy));
await tester.pumpAndSettle();
// Page 2 covers page 1.
expect(find.text('Page 1'), findsNothing);
expect(find.text('Page 2'), isOnstage);
tester.state<NavigatorState>(find.byType(Navigator)).pop();
await tester.pump();
await tester.pump(const Duration(milliseconds: 100));
widget1TransientTopLeft = tester.getTopLeft(find.text('Page 1'));
widget2TopLeft = tester.getTopLeft(find.text('Page 2'));
// Page 1 doesn't move.
expect(widget1TransientTopLeft, equals(widget1InitialTopLeft));
// Fullscreen dialogs transitions vertically only.
expect(widget1InitialTopLeft.dx, equals(widget2TopLeft.dx));
// Page 2 is leaving towards the bottom.
expect(widget2TopLeft.dy, greaterThan(widget1InitialTopLeft.dy));
await tester.pumpAndSettle();
expect(find.text('Page 1'), isOnstage);
expect(find.text('Page 2'), findsNothing);
widget1TransientTopLeft = tester.getTopLeft(find.text('Page 1'));
// Page 1 is back where it started.
expect(widget1InitialTopLeft, equals(widget1TransientTopLeft));
});
testWidgets('test only edge swipes work (LTR)', (WidgetTester tester) async {
await tester.pumpWidget(
CupertinoApp(
onGenerateRoute: (RouteSettings settings) {
return CupertinoPageRoute<void>(
settings: settings,
builder: (BuildContext context) {
final String pageNumber = settings.name == '/' ? '1' : '2';
return Center(child: Text('Page $pageNumber'));
},
);
},
),
);
tester.state<NavigatorState>(find.byType(Navigator)).pushNamed('/next');
await tester.pump();
await tester.pump(const Duration(seconds: 1));
// Page 2 covers page 1.
expect(find.text('Page 1'), findsNothing);
expect(find.text('Page 2'), isOnstage);
// Drag from the middle to the right.
TestGesture gesture = await tester.startGesture(const Offset(200.0, 200.0));
await gesture.moveBy(const Offset(300.0, 0.0));
await tester.pump();
// Nothing should happen.
expect(find.text('Page 1'), findsNothing);
expect(find.text('Page 2'), isOnstage);
// Drag from the right to the left.
gesture = await tester.startGesture(const Offset(795.0, 200.0));
await gesture.moveBy(const Offset(-300.0, 0.0));
await tester.pump();
// Nothing should happen.
expect(find.text('Page 1'), findsNothing);
expect(find.text('Page 2'), isOnstage);
// Drag from the right to the further right.
gesture = await tester.startGesture(const Offset(795.0, 200.0));
await gesture.moveBy(const Offset(300.0, 0.0));
await tester.pump();
// Nothing should happen.
expect(find.text('Page 1'), findsNothing);
expect(find.text('Page 2'), isOnstage);
// Now drag from the left edge.
gesture = await tester.startGesture(const Offset(5.0, 200.0));
await gesture.moveBy(const Offset(300.0, 0.0));
await tester.pump();
// Page 1 is now visible.
expect(find.text('Page 1'), isOnstage);
expect(find.text('Page 2'), isOnstage);
});
testWidgets('test edge swipes work with media query padding (LTR)', (WidgetTester tester) async {
await tester.pumpWidget(
CupertinoApp(
builder: (BuildContext context, Widget? navigator) {
return MediaQuery(
data: const MediaQueryData(padding: EdgeInsets.only(left: 40)),
child: navigator!,
);
},
home: const Placeholder(),
),
);
tester.state<NavigatorState>(find.byType(Navigator)).push(
CupertinoPageRoute<void>(
builder: (BuildContext context) => const Center(child: Text('Page 1')),
),
);
await tester.pump();
await tester.pump(const Duration(seconds: 2));
tester.state<NavigatorState>(find.byType(Navigator)).push(
CupertinoPageRoute<void>(
builder: (BuildContext context) => const Center(child: Text('Page 2')),
),
);
await tester.pump();
await tester.pumpAndSettle();
expect(find.text('Page 1'), findsNothing);
expect(find.text('Page 2'), isOnstage);
// Now drag from the left edge.
final TestGesture gesture = await tester.startGesture(const Offset(35.0, 200.0));
await gesture.moveBy(const Offset(300.0, 0.0));
await tester.pump();
await tester.pumpAndSettle();
// Page 1 is now visible.
expect(find.text('Page 1'), isOnstage);
expect(find.text('Page 2'), isOnstage);
});
testWidgets('test edge swipes work with media query padding (RLT)', (WidgetTester tester) async {
await tester.pumpWidget(
CupertinoApp(
builder: (BuildContext context, Widget? navigator) {
return Directionality(
textDirection: TextDirection.rtl,
child: MediaQuery(
data: const MediaQueryData(padding: EdgeInsets.only(right: 40)),
child: navigator!,
),
);
},
home: const Placeholder(),
),
);
tester.state<NavigatorState>(find.byType(Navigator)).push(
CupertinoPageRoute<void>(
builder: (BuildContext context) => const Center(child: Text('Page 1')),
),
);
await tester.pump();
await tester.pumpAndSettle();
tester.state<NavigatorState>(find.byType(Navigator)).push(
CupertinoPageRoute<void>(
builder: (BuildContext context) => const Center(child: Text('Page 2')),
),
);
await tester.pump();
await tester.pumpAndSettle();
expect(find.text('Page 1'), findsNothing);
expect(find.text('Page 2'), isOnstage);
// Now drag from the left edge.
final TestGesture gesture = await tester.startGesture(const Offset(765.0, 200.0));
await gesture.moveBy(const Offset(-300.0, 0.0));
await tester.pump();
await tester.pumpAndSettle();
// Page 1 is now visible.
expect(find.text('Page 1'), isOnstage);
expect(find.text('Page 2'), isOnstage);
});
testWidgets('test only edge swipes work (RTL)', (WidgetTester tester) async {
await tester.pumpWidget(
CupertinoApp(
localizationsDelegates: const <LocalizationsDelegate<dynamic>>[
RtlOverrideWidgetsDelegate(),
],
onGenerateRoute: (RouteSettings settings) {
return CupertinoPageRoute<void>(
settings: settings,
builder: (BuildContext context) {
final String pageNumber = settings.name == '/' ? '1' : '2';
return Center(child: Text('Page $pageNumber'));
},
);
},
),
);
await tester.pump(); // to load the localization, since it doesn't use a synchronous future
tester.state<NavigatorState>(find.byType(Navigator)).pushNamed('/next');
await tester.pump();
await tester.pumpAndSettle();
// Page 2 covers page 1.
expect(find.text('Page 1'), findsNothing);
expect(find.text('Page 2'), isOnstage);
// Drag from the middle to the left.
TestGesture gesture = await tester.startGesture(const Offset(200.0, 200.0));
await gesture.moveBy(const Offset(-300.0, 0.0));
await tester.pump();
// Nothing should happen.
expect(find.text('Page 1'), findsNothing);
expect(find.text('Page 2'), isOnstage);
// Drag from the left to the right.
gesture = await tester.startGesture(const Offset(5.0, 200.0));
await gesture.moveBy(const Offset(300.0, 0.0));
await tester.pump();
// Nothing should happen.
expect(find.text('Page 1'), findsNothing);
expect(find.text('Page 2'), isOnstage);
// Drag from the left to the further left.
gesture = await tester.startGesture(const Offset(5.0, 200.0));
await gesture.moveBy(const Offset(-300.0, 0.0));
await tester.pump();
// Nothing should happen.
expect(find.text('Page 1'), findsNothing);
expect(find.text('Page 2'), isOnstage);
// Now drag from the right edge.
gesture = await tester.startGesture(const Offset(795.0, 200.0));
await gesture.moveBy(const Offset(-300.0, 0.0));
await tester.pump();
// Page 1 is now visible.
expect(find.text('Page 1'), isOnstage);
expect(find.text('Page 2'), isOnstage);
});
testWidgets('test edge swipe then drop back at starting point works', (WidgetTester tester) async {
await tester.pumpWidget(
CupertinoApp(
onGenerateRoute: (RouteSettings settings) {
return CupertinoPageRoute<void>(
settings: settings,
builder: (BuildContext context) {
final String pageNumber = settings.name == '/' ? '1' : '2';
return Center(child: Text('Page $pageNumber'));
},
);
},
),
);
tester.state<NavigatorState>(find.byType(Navigator)).pushNamed('/next');
await tester.pump();
await tester.pump(const Duration(seconds: 1));
expect(find.text('Page 1'), findsNothing);
expect(find.text('Page 2'), isOnstage);
final TestGesture gesture = await tester.startGesture(const Offset(5, 200));
await gesture.moveBy(const Offset(300, 0));
await tester.pump();
// Bring it exactly back such that there's nothing to animate when releasing.
await gesture.moveBy(const Offset(-300, 0));
await gesture.up();
await tester.pump();
expect(find.text('Page 1'), findsNothing);
expect(find.text('Page 2'), isOnstage);
});
testWidgets('CupertinoPage does not lose its state when transitioning out', (WidgetTester tester) async {
final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>();
await tester.pumpWidget(KeepsStateTestWidget(navigatorKey: navigator));
expect(find.text('subpage'), findsOneWidget);
expect(find.text('home'), findsNothing);
navigator.currentState!.pop();
await tester.pump();
expect(find.text('subpage'), findsOneWidget);
expect(find.text('home'), findsOneWidget);
});
testWidgets('CupertinoPage restores its state', (WidgetTester tester) async {
await tester.pumpWidget(
RootRestorationScope(
restorationId: 'root',
child: MediaQuery(
data: const MediaQueryData(),
child: Directionality(
textDirection: TextDirection.ltr,
child: Navigator(
onPopPage: (Route<dynamic> route, dynamic result) { return false; },
pages: const <Page<Object?>>[
CupertinoPage<void>(
restorationId: 'p1',
child: TestRestorableWidget(restorationId: 'p1'),
),
],
restorationScopeId: 'nav',
onGenerateRoute: (RouteSettings settings) {
return CupertinoPageRoute<void>(
settings: settings,
builder: (BuildContext context) {
return TestRestorableWidget(restorationId: settings.name!);
},
);
},
),
),
),
),
);
expect(find.text('p1'), findsOneWidget);
expect(find.text('count: 0'), findsOneWidget);
await tester.tap(find.text('increment'));
await tester.pump();
expect(find.text('count: 1'), findsOneWidget);
tester.state<NavigatorState>(find.byType(Navigator)).restorablePushNamed('p2');
await tester.pumpAndSettle();
expect(find.text('p1'), findsNothing);
expect(find.text('p2'), findsOneWidget);
await tester.tap(find.text('increment'));
await tester.pump();
await tester.tap(find.text('increment'));
await tester.pump();
expect(find.text('count: 2'), findsOneWidget);
await tester.restartAndRestore();
expect(find.text('p2'), findsOneWidget);
expect(find.text('count: 2'), findsOneWidget);
tester.state<NavigatorState>(find.byType(Navigator)).pop();
await tester.pumpAndSettle();
expect(find.text('p1'), findsOneWidget);
expect(find.text('count: 1'), findsOneWidget);
});
}
class RtlOverrideWidgetsDelegate extends LocalizationsDelegate<WidgetsLocalizations> {
const RtlOverrideWidgetsDelegate();
@override
bool isSupported(Locale locale) => true;
@override
Future<WidgetsLocalizations> load(Locale locale) async => const RtlOverrideWidgetsLocalization();
@override
bool shouldReload(LocalizationsDelegate<WidgetsLocalizations> oldDelegate) => false;
}
class RtlOverrideWidgetsLocalization extends DefaultWidgetsLocalizations {
const RtlOverrideWidgetsLocalization();
@override
TextDirection get textDirection => TextDirection.rtl;
}
class KeepsStateTestWidget extends StatefulWidget {
const KeepsStateTestWidget({super.key, this.navigatorKey});
final Key? navigatorKey;
@override
State<KeepsStateTestWidget> createState() => _KeepsStateTestWidgetState();
}
class _KeepsStateTestWidgetState extends State<KeepsStateTestWidget> {
String? _subpage = 'subpage';
@override
Widget build(BuildContext context) {
return CupertinoApp(
home: Navigator(
key: widget.navigatorKey,
pages: <Page<void>>[
const CupertinoPage<void>(child: Text('home')),
if (_subpage != null) CupertinoPage<void>(child: Text(_subpage!)),
],
onPopPage: (Route<dynamic> route, dynamic result) {
if (!route.didPop(result)) {
return false;
}
setState(() {
_subpage = null;
});
return true;
},
),
);
}
}
class TestRestorableWidget extends StatefulWidget {
const TestRestorableWidget({super.key, required this.restorationId});
final String restorationId;
@override
State<StatefulWidget> createState() => _TestRestorableWidgetState();
}
class _TestRestorableWidgetState extends State<TestRestorableWidget> with RestorationMixin {
@override
String? get restorationId => widget.restorationId;
final RestorableInt counter = RestorableInt(0);
@override
void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
registerForRestoration(counter, 'counter');
}
@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
Text(widget.restorationId),
Text('count: ${counter.value}'),
CupertinoButton(
onPressed: () {
setState(() {
counter.value++;
});
},
child: const Text('increment'),
),
],
);
}
}