blob: dc4dbebc38ff8f07e69b7eae19199933c32a2348 [file] [log] [blame] [edit]
// 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.
@TestOn('!chrome')
// TODO(gspencergoog): Remove this tag once this test's state leaks/test
// dependencies have been fixed.
// https://github.com/flutter/flutter/issues/85160
// Fails with "flutter test --test-randomize-ordering-seed=456"
@Tags(<String>['no-shuffle'])
import 'package:flutter/cupertino.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
Future<void> startTransitionBetween(
WidgetTester tester, {
Widget? from,
Widget? to,
String? fromTitle,
String? toTitle,
TextDirection textDirection = TextDirection.ltr,
CupertinoThemeData? theme,
}) async {
await tester.pumpWidget(
CupertinoApp(
theme: theme,
builder: (BuildContext context, Widget? navigator) {
return Directionality(
textDirection: textDirection,
child: navigator!,
);
},
home: const Placeholder(),
),
);
tester
.state<NavigatorState>(find.byType(Navigator))
.push(CupertinoPageRoute<void>(
title: fromTitle,
builder: (BuildContext context) => scaffoldForNavBar(from)!,
));
await tester.pump();
await tester.pump(const Duration(milliseconds: 500));
tester
.state<NavigatorState>(find.byType(Navigator))
.push(CupertinoPageRoute<void>(
title: toTitle,
builder: (BuildContext context) => scaffoldForNavBar(to)!,
));
await tester.pump();
}
CupertinoPageScaffold? scaffoldForNavBar(Widget? navBar) {
if (navBar is CupertinoNavigationBar || navBar == null) {
return CupertinoPageScaffold(
navigationBar: navBar as CupertinoNavigationBar? ?? const CupertinoNavigationBar(),
child: const Placeholder(),
);
} else if (navBar is CupertinoSliverNavigationBar) {
return CupertinoPageScaffold(
child: CustomScrollView(
slivers: <Widget>[
navBar,
// Add filler so it's scrollable.
const SliverToBoxAdapter(
child: Placeholder(fallbackHeight: 1000.0),
),
],
),
);
}
assert(false, 'Unexpected nav bar type ${navBar.runtimeType}');
return null;
}
Finder flying(WidgetTester tester, Finder finder) {
final ContainerRenderObjectMixin<RenderBox, StackParentData> theater = tester.renderObject(find.byType(Overlay));
final Finder lastOverlayFinder = find.byElementPredicate((Element element) {
return element is RenderObjectElement && element.renderObject == theater.lastChild;
});
assert(
find.descendant(
of: lastOverlayFinder,
matching: find.byWidgetPredicate(
(Widget widget) =>
widget.runtimeType.toString() == '_NavigationBarTransition',
),
).evaluate().length == 1,
'The last overlay in the navigator was not a flying hero',
);
return find.descendant(
of: lastOverlayFinder,
matching: finder,
);
}
void checkBackgroundBoxHeight(WidgetTester tester, double height) {
final Widget transitionBackgroundBox =
tester.widget<Stack>(flying(tester, find.byType(Stack))).children[0];
expect(
tester.widget<SizedBox>(
find.descendant(
of: find.byWidget(transitionBackgroundBox),
matching: find.byType(SizedBox),
),
).height,
height,
);
}
void checkOpacity(WidgetTester tester, Finder finder, double opacity) {
expect(
tester.firstRenderObject<RenderAnimatedOpacity>(
find.ancestor(
of: finder,
matching: find.byType(FadeTransition),
),
).opacity.value,
moreOrLessEquals(opacity),
);
}
void main() {
testWidgets('Bottom middle moves between middle and back label', (WidgetTester tester) async {
await startTransitionBetween(tester, fromTitle: 'Page 1');
// Be mid-transition.
await tester.pump(const Duration(milliseconds: 50));
// There's 2 of them. One from the top route's back label and one from the
// bottom route's middle widget.
expect(flying(tester, find.text('Page 1')), findsNWidgets(2));
// Since they have the same text, they should be more or less at the same
// place.
expect(
tester.getTopLeft(flying(tester, find.text('Page 1')).first),
const Offset(337.1953125, 13.5),
);
expect(
tester.getTopLeft(flying(tester, find.text('Page 1')).last),
const Offset(337.1953125, 13.5),
);
});
testWidgets('Bottom middle moves between middle and back label RTL', (WidgetTester tester) async {
await startTransitionBetween(
tester,
fromTitle: 'Page 1',
textDirection: TextDirection.rtl,
);
await tester.pump(const Duration(milliseconds: 50));
expect(flying(tester, find.text('Page 1')), findsNWidgets(2));
// Same as LTR but more to the right now.
expect(
tester.getTopLeft(flying(tester, find.text('Page 1')).first),
const Offset(362.8046875, 13.5),
);
expect(
tester.getTopLeft(flying(tester, find.text('Page 1')).last),
const Offset(362.8046875, 13.5),
);
});
testWidgets('Bottom middle never changes size during the animation', (WidgetTester tester) async {
await tester.binding.setSurfaceSize(const Size(1080.0 / 2.75, 600));
addTearDown(() async {
await tester.binding.setSurfaceSize(const Size(800.0, 600.0));
});
await startTransitionBetween(
tester,
fromTitle: 'Page 1',
);
final Size size = tester.getSize(find.text('Page 1'));
for (int i = 0; i < 150; i++) {
await tester.pump(const Duration(milliseconds: 1));
expect(flying(tester, find.text('Page 1')), findsNWidgets(2));
expect(tester.getSize(flying(tester, find.text('Page 1')).first), size);
expect(tester.getSize(flying(tester, find.text('Page 1')).last), size);
}
});
testWidgets('Bottom middle and top back label transitions their font', (WidgetTester tester) async {
await startTransitionBetween(tester, fromTitle: 'Page 1');
// Be mid-transition.
await tester.pump(const Duration(milliseconds: 50));
// The transition's stack is ordered. The bottom middle is inserted first.
final RenderParagraph bottomMiddle =
tester.renderObject(flying(tester, find.text('Page 1')).first);
expect(bottomMiddle.text.style!.color, const Color(0xff00050a));
expect(bottomMiddle.text.style!.fontWeight, FontWeight.w600);
expect(bottomMiddle.text.style!.fontFamily, '.SF Pro Text');
expect(bottomMiddle.text.style!.letterSpacing, -0.41);
checkOpacity(tester, flying(tester, find.text('Page 1')).first, 0.9004602432250977);
// The top back label is styled exactly the same way. But the opacity tweens
// are flipped.
final RenderParagraph topBackLabel =
tester.renderObject(flying(tester, find.text('Page 1')).last);
expect(topBackLabel.text.style!.color, const Color(0xff00050a));
expect(topBackLabel.text.style!.fontWeight, FontWeight.w600);
expect(topBackLabel.text.style!.fontFamily, '.SF Pro Text');
expect(topBackLabel.text.style!.letterSpacing, -0.41);
checkOpacity(tester, flying(tester, find.text('Page 1')).last, 0.0);
// Move animation further a bit.
await tester.pump(const Duration(milliseconds: 200));
expect(bottomMiddle.text.style!.color, const Color(0xff006de4));
expect(bottomMiddle.text.style!.fontWeight, FontWeight.w400);
expect(bottomMiddle.text.style!.fontFamily, '.SF Pro Text');
expect(bottomMiddle.text.style!.letterSpacing, -0.41);
checkOpacity(tester, flying(tester, find.text('Page 1')).first, 0.0);
expect(topBackLabel.text.style!.color, const Color(0xff006de4));
expect(topBackLabel.text.style!.fontWeight, FontWeight.w400);
expect(topBackLabel.text.style!.fontFamily, '.SF Pro Text');
expect(topBackLabel.text.style!.letterSpacing, -0.41);
checkOpacity(tester, flying(tester, find.text('Page 1')).last, 0.7630139589309692);
});
testWidgets('Font transitions respect themes', (WidgetTester tester) async {
await startTransitionBetween(
tester,
fromTitle: 'Page 1',
theme: const CupertinoThemeData(brightness: Brightness.dark),
);
// Be mid-transition.
await tester.pump(const Duration(milliseconds: 50));
// The transition's stack is ordered. The bottom middle is inserted first.
final RenderParagraph bottomMiddle =
tester.renderObject(flying(tester, find.text('Page 1')).first);
expect(bottomMiddle.text.style!.color, const Color(0xFFF4F9FF));
expect(bottomMiddle.text.style!.fontWeight, FontWeight.w600);
expect(bottomMiddle.text.style!.fontFamily, '.SF Pro Text');
expect(bottomMiddle.text.style!.letterSpacing, -0.41);
checkOpacity(tester, flying(tester, find.text('Page 1')).first, 0.9004602432250977);
// The top back label is styled exactly the same way. But the opacity tweens
// are flipped.
final RenderParagraph topBackLabel =
tester.renderObject(flying(tester, find.text('Page 1')).last);
expect(topBackLabel.text.style!.color, const Color(0xFFF4F9FF));
expect(topBackLabel.text.style!.fontWeight, FontWeight.w600);
expect(topBackLabel.text.style!.fontFamily, '.SF Pro Text');
expect(topBackLabel.text.style!.letterSpacing, -0.41);
checkOpacity(tester, flying(tester, find.text('Page 1')).last, 0.0);
// Move animation further a bit.
await tester.pump(const Duration(milliseconds: 200));
expect(bottomMiddle.text.style!.color, const Color(0xFF2390FF));
expect(bottomMiddle.text.style!.fontWeight, FontWeight.w400);
expect(bottomMiddle.text.style!.fontFamily, '.SF Pro Text');
expect(bottomMiddle.text.style!.letterSpacing, -0.41);
checkOpacity(tester, flying(tester, find.text('Page 1')).first, 0.0);
expect(topBackLabel.text.style!.color, const Color(0xFF2390FF));
expect(topBackLabel.text.style!.fontWeight, FontWeight.w400);
expect(topBackLabel.text.style!.fontFamily, '.SF Pro Text');
expect(topBackLabel.text.style!.letterSpacing, -0.41);
checkOpacity(tester, flying(tester, find.text('Page 1')).last, 0.7630139589309692);
});
testWidgets('Fullscreen dialogs do not create heroes', (WidgetTester tester) async {
await tester.pumpWidget(
const CupertinoApp(
home: Placeholder(),
),
);
tester
.state<NavigatorState>(find.byType(Navigator))
.push(CupertinoPageRoute<void>(
title: 'Page 1',
builder: (BuildContext context) => scaffoldForNavBar(null)!,
));
await tester.pump();
await tester.pump(const Duration(milliseconds: 500));
tester
.state<NavigatorState>(find.byType(Navigator))
.push(CupertinoPageRoute<void>(
title: 'Page 2',
fullscreenDialog: true,
builder: (BuildContext context) => scaffoldForNavBar(null)!,
));
await tester.pump();
await tester.pump(const Duration(milliseconds: 100));
// Only the first (non-fullscreen-dialog) page has a Hero.
expect(find.byType(Hero), findsOneWidget);
// No Hero transition happened.
expect(() => flying(tester, find.text('Page 2')), throwsAssertionError);
});
testWidgets('Turning off transition works', (WidgetTester tester) async {
await startTransitionBetween(
tester,
from: const CupertinoNavigationBar(
transitionBetweenRoutes: false,
middle: Text('Page 1'),
),
toTitle: 'Page 2',
);
await tester.pump(const Duration(milliseconds: 50));
// Only the second page that doesn't have the transitionBetweenRoutes
// override off has a Hero.
expect(find.byType(Hero), findsOneWidget);
expect(
find.descendant(of: find.byType(Hero), matching: find.text('Page 2')),
findsOneWidget,
);
// No Hero transition happened.
expect(() => flying(tester, find.text('Page 2')), throwsAssertionError);
});
testWidgets('Popping mid-transition is symmetrical', (WidgetTester tester) async {
await startTransitionBetween(tester, fromTitle: 'Page 1');
// Be mid-transition.
await tester.pump(const Duration(milliseconds: 50));
void checkColorAndPositionAt50ms() {
// The transition's stack is ordered. The bottom middle is inserted first.
final RenderParagraph bottomMiddle =
tester.renderObject(flying(tester, find.text('Page 1')).first);
expect(bottomMiddle.text.style!.color, const Color(0xff00050a));
expect(
tester.getTopLeft(flying(tester, find.text('Page 1')).first),
const Offset(337.1953125, 13.5),
);
// The top back label is styled exactly the same way. But the opacity tweens
// are flipped.
final RenderParagraph topBackLabel =
tester.renderObject(flying(tester, find.text('Page 1')).last);
expect(topBackLabel.text.style!.color, const Color(0xff00050a));
expect(
tester.getTopLeft(flying(tester, find.text('Page 1')).last),
const Offset(337.1953125, 13.5),
);
}
checkColorAndPositionAt50ms();
// Advance more.
await tester.pump(const Duration(milliseconds: 100));
// Pop and reverse the same amount of time.
tester.state<NavigatorState>(find.byType(Navigator)).pop();
await tester.pump();
await tester.pump(const Duration(milliseconds: 100));
// Check that everything's the same as on the way in.
checkColorAndPositionAt50ms();
});
testWidgets('Popping mid-transition is symmetrical RTL', (WidgetTester tester) async {
await startTransitionBetween(
tester,
fromTitle: 'Page 1',
textDirection: TextDirection.rtl,
);
// Be mid-transition.
await tester.pump(const Duration(milliseconds: 50));
void checkColorAndPositionAt50ms() {
// The transition's stack is ordered. The bottom middle is inserted first.
final RenderParagraph bottomMiddle =
tester.renderObject(flying(tester, find.text('Page 1')).first);
expect(bottomMiddle.text.style!.color, const Color(0xff00050a));
expect(
tester.getTopLeft(flying(tester, find.text('Page 1')).first),
const Offset(362.8046875, 13.5),
);
// The top back label is styled exactly the same way. But the opacity tweens
// are flipped.
final RenderParagraph topBackLabel =
tester.renderObject(flying(tester, find.text('Page 1')).last);
expect(topBackLabel.text.style!.color, const Color(0xff00050a));
expect(
tester.getTopLeft(flying(tester, find.text('Page 1')).last),
const Offset(362.8046875, 13.5),
);
}
checkColorAndPositionAt50ms();
// Advance more.
await tester.pump(const Duration(milliseconds: 100));
// Pop and reverse the same amount of time.
tester.state<NavigatorState>(find.byType(Navigator)).pop();
await tester.pump();
await tester.pump(const Duration(milliseconds: 100));
// Check that everything's the same as on the way in.
checkColorAndPositionAt50ms();
});
testWidgets('There should be no global keys in the hero flight', (WidgetTester tester) async {
await startTransitionBetween(tester, fromTitle: 'Page 1');
// Be mid-transition.
await tester.pump(const Duration(milliseconds: 50));
expect(
flying(
tester,
find.byWidgetPredicate((Widget widget) => widget.key != null),
),
findsNothing,
);
});
testWidgets('Multiple nav bars tags do not conflict if in different navigators', (WidgetTester tester) async {
await tester.pumpWidget(
CupertinoApp(
home: CupertinoTabScaffold(
tabBar: CupertinoTabBar(
items: const <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: Icon(CupertinoIcons.search),
label: 'Tab 1',
),
BottomNavigationBarItem(
icon: Icon(CupertinoIcons.settings),
label: 'Tab 2',
),
],
),
tabBuilder: (BuildContext context, int tab) {
return CupertinoTabView(
builder: (BuildContext context) {
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
middle: Text('Tab ${tab + 1} Page 1'),
),
child: Center(
child: CupertinoButton(
child: const Text('Next'),
onPressed: () {
Navigator.push<void>(context, CupertinoPageRoute<void>(
title: 'Tab ${tab + 1} Page 2',
builder: (BuildContext context) {
return const CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(),
child: Placeholder(),
);
},
));
},
),
),
);
},
);
},
),
),
);
await tester.tap(find.text('Tab 2'));
await tester.pump();
expect(find.text('Tab 1 Page 1', skipOffstage: false), findsOneWidget);
expect(find.text('Tab 2 Page 1'), findsOneWidget);
// At this point, there are 2 nav bars seeded with the same _defaultHeroTag.
// But they're inside different navigators.
await tester.tap(find.text('Next'));
await tester.pump();
await tester.pump(const Duration(milliseconds: 200));
// One is inside the flight shuttle and another is invisible in the
// incoming route in case a new flight needs to be created midflight.
expect(find.text('Tab 2 Page 2'), findsNWidgets(2));
await tester.pump(const Duration(milliseconds: 500));
expect(find.text('Tab 2 Page 2'), findsOneWidget);
// Offstaged by tab 2's navigator.
expect(find.text('Tab 2 Page 1', skipOffstage: false), findsOneWidget);
// Offstaged by the CupertinoTabScaffold.
expect(find.text('Tab 1 Page 1', skipOffstage: false), findsOneWidget);
// Never navigated to tab 1 page 2.
expect(find.text('Tab 1 Page 2', skipOffstage: false), findsNothing);
});
testWidgets('Transition box grows to large title size', (WidgetTester tester) async {
await startTransitionBetween(
tester,
fromTitle: 'Page 1',
to: const CupertinoSliverNavigationBar(),
toTitle: 'Page 2',
);
await tester.pump(const Duration(milliseconds: 50));
checkBackgroundBoxHeight(tester, 46.234375);
await tester.pump(const Duration(milliseconds: 50));
checkBackgroundBoxHeight(tester, 56.3232741355896);
await tester.pump(const Duration(milliseconds: 50));
checkBackgroundBoxHeight(tester, 73.04067611694336);
await tester.pump(const Duration(milliseconds: 50));
checkBackgroundBoxHeight(tester, 84.33018499612808);
await tester.pump(const Duration(milliseconds: 50));
checkBackgroundBoxHeight(tester, 90.53337162733078);
});
testWidgets('Large transition box shrinks to standard nav bar size', (WidgetTester tester) async {
await startTransitionBetween(
tester,
from: const CupertinoSliverNavigationBar(),
fromTitle: 'Page 1',
toTitle: 'Page 2',
);
await tester.pump(const Duration(milliseconds: 50));
checkBackgroundBoxHeight(tester, 93.765625);
await tester.pump(const Duration(milliseconds: 50));
checkBackgroundBoxHeight(tester, 83.6767258644104);
await tester.pump(const Duration(milliseconds: 50));
checkBackgroundBoxHeight(tester, 66.95932388305664);
await tester.pump(const Duration(milliseconds: 50));
checkBackgroundBoxHeight(tester, 55.66981500387192);
await tester.pump(const Duration(milliseconds: 50));
checkBackgroundBoxHeight(tester, 49.46662837266922);
});
testWidgets('Hero flight removed at the end of page transition', (WidgetTester tester) async {
await startTransitionBetween(tester, fromTitle: 'Page 1');
await tester.pump(const Duration(milliseconds: 50));
// There's 2 of them. One from the top route's back label and one from the
// bottom route's middle widget.
expect(flying(tester, find.text('Page 1')), findsNWidgets(2));
// End the transition.
await tester.pump(const Duration(milliseconds: 500));
expect(() => flying(tester, find.text('Page 1')), throwsAssertionError);
});
testWidgets('Exact widget is reused to build inside the transition', (WidgetTester tester) async {
const Widget userMiddle = Placeholder();
await startTransitionBetween(
tester,
from: const CupertinoSliverNavigationBar(
middle: userMiddle,
),
fromTitle: 'Page 1',
toTitle: 'Page 2',
);
await tester.pump(const Duration(milliseconds: 50));
expect(flying(tester, find.byWidget(userMiddle)), findsOneWidget);
});
testWidgets('First appearance of back chevron fades in from the right', (WidgetTester tester) async {
await tester.pumpWidget(
CupertinoApp(
home: scaffoldForNavBar(null),
),
);
tester
.state<NavigatorState>(find.byType(Navigator))
.push(CupertinoPageRoute<void>(
title: 'Page 1',
builder: (BuildContext context) => scaffoldForNavBar(null)!,
));
await tester.pump();
await tester.pump(const Duration(milliseconds: 50));
final Finder backChevron = flying(tester, find.text(String.fromCharCode(CupertinoIcons.back.codePoint)));
expect(
backChevron,
// Only one exists from the top page. The bottom page has no back chevron.
findsOneWidget,
);
// Come in from the right and fade in.
checkOpacity(tester, backChevron, 0.0);
expect(tester.getTopLeft(backChevron), const Offset(86.734375, 7.0));
await tester.pump(const Duration(milliseconds: 150));
checkOpacity(tester, backChevron, 0.09497911669313908);
expect(tester.getTopLeft(backChevron), const Offset(31.055883467197418, 7.0));
});
testWidgets('First appearance of back chevron fades in from the left in RTL', (WidgetTester tester) async {
await tester.pumpWidget(
CupertinoApp(
builder: (BuildContext context, Widget? navigator) {
return Directionality(
textDirection: TextDirection.rtl,
child: navigator!,
);
},
home: scaffoldForNavBar(null),
),
);
tester
.state<NavigatorState>(find.byType(Navigator))
.push(CupertinoPageRoute<void>(
title: 'Page 1',
builder: (BuildContext context) => scaffoldForNavBar(null)!,
));
await tester.pump();
await tester.pump(const Duration(milliseconds: 50));
final Finder backChevron = flying(tester, find.text(String.fromCharCode(CupertinoIcons.back.codePoint)));
expect(
backChevron,
// Only one exists from the top page. The bottom page has no back chevron.
findsOneWidget,
);
// Come in from the right and fade in.
checkOpacity(tester, backChevron, 0.0);
expect(
tester.getTopRight(backChevron),
const Offset(687.265625, 7.0),
);
await tester.pump(const Duration(milliseconds: 150));
checkOpacity(tester, backChevron, 0.09497911669313908);
expect(
tester.getTopRight(backChevron),
const Offset(742.9441165328026, 7.0),
);
});
testWidgets('Back chevron fades out and in when both pages have it', (WidgetTester tester) async {
await startTransitionBetween(tester, fromTitle: 'Page 1');
await tester.pump(const Duration(milliseconds: 50));
final Finder backChevrons = flying(tester, find.text(String.fromCharCode(CupertinoIcons.back.codePoint)));
expect(
backChevrons,
findsNWidgets(2),
);
checkOpacity(tester, backChevrons.first, 0.8833301812410355);
checkOpacity(tester, backChevrons.last, 0.0);
// Both overlap at the same place.
expect(tester.getTopLeft(backChevrons.first), const Offset(14.0, 7.0));
expect(tester.getTopLeft(backChevrons.last), const Offset(14.0, 7.0));
await tester.pump(const Duration(milliseconds: 150));
checkOpacity(tester, backChevrons.first, 0.0);
checkOpacity(tester, backChevrons.last, 0.4604858811944723);
// Still in the same place.
expect(tester.getTopLeft(backChevrons.first), const Offset(14.0, 7.0));
expect(tester.getTopLeft(backChevrons.last), const Offset(14.0, 7.0));
});
testWidgets('Bottom middle just fades if top page has a custom leading', (WidgetTester tester) async {
await startTransitionBetween(
tester,
fromTitle: 'Page 1',
to: const CupertinoSliverNavigationBar(
leading: Text('custom'),
),
toTitle: 'Page 2',
);
await tester.pump(const Duration(milliseconds: 50));
// There's just 1 in flight because there's no back label on the top page.
expect(flying(tester, find.text('Page 1')), findsOneWidget);
checkOpacity(tester, flying(tester, find.text('Page 1')), 0.9004602432250977);
// The middle widget doesn't move.
expect(
tester.getCenter(flying(tester, find.text('Page 1'))),
const Offset(400.0, 22.0),
);
await tester.pump(const Duration(milliseconds: 150));
checkOpacity(tester, flying(tester, find.text('Page 1')), 0.0);
expect(
tester.getCenter(flying(tester, find.text('Page 1'))),
const Offset(400.0, 22.0),
);
});
testWidgets('Bottom leading fades in place', (WidgetTester tester) async {
await startTransitionBetween(
tester,
from: const CupertinoSliverNavigationBar(leading: Text('custom')),
fromTitle: 'Page 1',
toTitle: 'Page 2',
);
await tester.pump(const Duration(milliseconds: 50));
expect(flying(tester, find.text('custom')), findsOneWidget);
checkOpacity(tester, flying(tester, find.text('custom')), 0.828093871474266);
expect(
tester.getTopLeft(flying(tester, find.text('custom'))),
const Offset(16.0, 0.0),
);
await tester.pump(const Duration(milliseconds: 150));
checkOpacity(tester, flying(tester, find.text('custom')), 0.0);
expect(
tester.getTopLeft(flying(tester, find.text('custom'))),
const Offset(16.0, 0.0),
);
});
testWidgets('Bottom trailing fades in place', (WidgetTester tester) async {
await startTransitionBetween(
tester,
from: const CupertinoSliverNavigationBar(trailing: Text('custom')),
fromTitle: 'Page 1',
toTitle: 'Page 2',
);
await tester.pump(const Duration(milliseconds: 50));
expect(flying(tester, find.text('custom')), findsOneWidget);
checkOpacity(tester, flying(tester, find.text('custom')), 0.8833301812410355);
expect(
tester.getTopLeft(flying(tester, find.text('custom'))),
const Offset(684.0, 13.5),
);
await tester.pump(const Duration(milliseconds: 150));
checkOpacity(tester, flying(tester, find.text('custom')), 0.0);
expect(
tester.getTopLeft(flying(tester, find.text('custom'))),
const Offset(684.0, 13.5),
);
});
testWidgets('Bottom back label fades and slides to the left', (WidgetTester tester) async {
await startTransitionBetween(
tester,
fromTitle: 'Page 1',
toTitle: 'Page 2',
);
await tester.pump(const Duration(milliseconds: 500));
tester
.state<NavigatorState>(find.byType(Navigator))
.push(CupertinoPageRoute<void>(
title: 'Page 3',
builder: (BuildContext context) => scaffoldForNavBar(null)!,
));
await tester.pump();
await tester.pump(const Duration(milliseconds: 50));
// 'Page 1' appears once on Page 2 as the back label.
expect(flying(tester, find.text('Page 1')), findsOneWidget);
// Back label fades out faster.
checkOpacity(tester, flying(tester, find.text('Page 1')), 0.6697911769151688);
expect(
tester.getTopLeft(flying(tester, find.text('Page 1'))),
const Offset(34.8125, 13.5),
);
await tester.pump(const Duration(milliseconds: 150));
checkOpacity(tester, flying(tester, find.text('Page 1')), 0.0);
expect(
tester.getTopLeft(flying(tester, find.text('Page 1'))),
const Offset(-258.2321922779083, 13.5),
);
});
testWidgets('Bottom back label fades and slides to the right in RTL', (WidgetTester tester) async {
await startTransitionBetween(
tester,
fromTitle: 'Page 1',
toTitle: 'Page 2',
textDirection: TextDirection.rtl,
);
await tester.pump(const Duration(milliseconds: 500));
tester
.state<NavigatorState>(find.byType(Navigator))
.push(CupertinoPageRoute<void>(
title: 'Page 3',
builder: (BuildContext context) => scaffoldForNavBar(null)!,
));
await tester.pump();
await tester.pump(const Duration(milliseconds: 50));
// 'Page 1' appears once on Page 2 as the back label.
expect(flying(tester, find.text('Page 1')), findsOneWidget);
// Back label fades out faster.
checkOpacity(tester, flying(tester, find.text('Page 1')), 0.6697911769151688);
expect(
tester.getTopRight(flying(tester, find.text('Page 1'))),
const Offset(765.1875, 13.5),
);
await tester.pump(const Duration(milliseconds: 150));
checkOpacity(tester, flying(tester, find.text('Page 1')), 0.0);
expect(
tester.getTopRight(flying(tester, find.text('Page 1'))),
// >1000. It's now off the screen.
const Offset(1058.2321922779083, 13.5),
);
});
testWidgets('Bottom large title moves to top back label', (WidgetTester tester) async {
await startTransitionBetween(
tester,
from: const CupertinoSliverNavigationBar(),
fromTitle: 'Page 1',
toTitle: 'Page 2',
);
await tester.pump(const Duration(milliseconds: 50));
// There's 2, one from the bottom large title fading out and one from the
// bottom back label fading in.
expect(flying(tester, find.text('Page 1')), findsNWidgets(2));
checkOpacity(tester, flying(tester, find.text('Page 1')).first, 0.8833301812410355);
checkOpacity(tester, flying(tester, find.text('Page 1')).last, 0.0);
expect(
tester.getTopLeft(flying(tester, find.text('Page 1')).first),
const Offset(17.546875, 52.39453125),
);
expect(
tester.getTopLeft(flying(tester, find.text('Page 1')).last),
const Offset(17.546875, 52.39453125),
);
await tester.pump(const Duration(milliseconds: 150));
checkOpacity(tester, flying(tester, find.text('Page 1')).first, 0.0);
checkOpacity(tester, flying(tester, find.text('Page 1')).last, 0.4604858811944723);
expect(
tester.getTopLeft(flying(tester, find.text('Page 1')).first),
const Offset(43.92089730501175, 22.49655644595623),
);
expect(
tester.getTopLeft(flying(tester, find.text('Page 1')).last),
const Offset(43.92089730501175, 22.49655644595623),
);
});
testWidgets('Long title turns into the word back mid transition', (WidgetTester tester) async {
await startTransitionBetween(
tester,
from: const CupertinoSliverNavigationBar(),
fromTitle: 'A title too long to fit',
toTitle: 'Page 2',
);
await tester.pump(const Duration(milliseconds: 50));
expect(flying(tester, find.text('A title too long to fit')), findsOneWidget);
// Automatically changed to the word 'Back' in the back label.
expect(flying(tester, find.text('Back')), findsOneWidget);
checkOpacity(tester, flying(tester, find.text('A title too long to fit')), 0.8833301812410355);
checkOpacity(tester, flying(tester, find.text('Back')), 0.0);
expect(
tester.getTopLeft(flying(tester, find.text('A title too long to fit'))),
const Offset(17.546875, 52.39453125),
);
expect(
tester.getTopLeft(flying(tester, find.text('Back'))),
const Offset(17.546875, 52.39453125),
);
await tester.pump(const Duration(milliseconds: 150));
checkOpacity(tester, flying(tester, find.text('A title too long to fit')), 0.0);
checkOpacity(tester, flying(tester, find.text('Back')), 0.4604858811944723);
expect(
tester.getTopLeft(flying(tester, find.text('A title too long to fit'))),
const Offset(43.92089730501175, 22.49655644595623),
);
expect(
tester.getTopLeft(flying(tester, find.text('Back'))),
const Offset(43.92089730501175, 22.49655644595623),
);
});
testWidgets('Bottom large title and top back label transitions their font', (WidgetTester tester) async {
await startTransitionBetween(
tester,
from: const CupertinoSliverNavigationBar(),
fromTitle: 'Page 1',
);
// Be mid-transition.
await tester.pump(const Duration(milliseconds: 50));
// The transition's stack is ordered. The bottom large title is inserted first.
final RenderParagraph bottomLargeTitle =
tester.renderObject(flying(tester, find.text('Page 1')).first);
expect(bottomLargeTitle.text.style!.color, const Color(0xff00050a));
expect(bottomLargeTitle.text.style!.fontWeight, FontWeight.w700);
expect(bottomLargeTitle.text.style!.fontFamily, '.SF Pro Display');
expect(bottomLargeTitle.text.style!.letterSpacing, moreOrLessEquals(0.374765625));
// The top back label is styled exactly the same way.
final RenderParagraph topBackLabel =
tester.renderObject(flying(tester, find.text('Page 1')).last);
expect(topBackLabel.text.style!.color, const Color(0xff00050a));
expect(topBackLabel.text.style!.fontWeight, FontWeight.w700);
expect(topBackLabel.text.style!.fontFamily, '.SF Pro Display');
expect(topBackLabel.text.style!.letterSpacing, moreOrLessEquals(0.374765625));
// Move animation further a bit.
await tester.pump(const Duration(milliseconds: 200));
expect(bottomLargeTitle.text.style!.color, const Color(0xff006de4));
expect(bottomLargeTitle.text.style!.fontWeight, FontWeight.w400);
expect(bottomLargeTitle.text.style!.fontFamily, '.SF Pro Text');
expect(bottomLargeTitle.text.style!.letterSpacing, moreOrLessEquals(-0.32379547566175454));
expect(topBackLabel.text.style!.color, const Color(0xff006de4));
expect(topBackLabel.text.style!.fontWeight, FontWeight.w400);
expect(topBackLabel.text.style!.fontFamily, '.SF Pro Text');
expect(topBackLabel.text.style!.letterSpacing, moreOrLessEquals(-0.32379547566175454));
});
testWidgets('Top middle fades in and slides in from the right', (WidgetTester tester) async {
await startTransitionBetween(
tester,
toTitle: 'Page 2',
);
await tester.pump(const Duration(milliseconds: 50));
expect(flying(tester, find.text('Page 2')), findsOneWidget);
checkOpacity(tester, flying(tester, find.text('Page 2')), 0.0);
expect(
tester.getTopLeft(flying(tester, find.text('Page 2'))),
const Offset(732.8125, 13.5),
);
await tester.pump(const Duration(milliseconds: 150));
checkOpacity(tester, flying(tester, find.text('Page 2')), 0.5555618554353714);
expect(
tester.getTopLeft(flying(tester, find.text('Page 2'))),
const Offset(439.7678077220917, 13.5),
);
});
testWidgets('Top middle never changes size during the animation', (WidgetTester tester) async {
await tester.binding.setSurfaceSize(const Size(1080.0 / 2.75, 600));
addTearDown(() async {
await tester.binding.setSurfaceSize(const Size(800.0, 600.0));
});
await startTransitionBetween(
tester,
toTitle: 'Page 2',
);
Size? previousSize;
for (int i = 0; i < 150; i++) {
await tester.pump(const Duration(milliseconds: 1));
expect(flying(tester, find.text('Page 2')), findsOneWidget);
final Size size = tester.getSize(flying(tester, find.text('Page 2')));
if (previousSize != null) {
expect(size, previousSize);
}
previousSize = size;
}
});
testWidgets('Top middle fades in and slides in from the left in RTL', (WidgetTester tester) async {
await startTransitionBetween(
tester,
toTitle: 'Page 2',
textDirection: TextDirection.rtl,
);
await tester.pump(const Duration(milliseconds: 50));
expect(flying(tester, find.text('Page 2')), findsOneWidget);
checkOpacity(tester, flying(tester, find.text('Page 2')), 0.0);
expect(
tester.getTopRight(flying(tester, find.text('Page 2'))),
const Offset(67.1875, 13.5),
);
await tester.pump(const Duration(milliseconds: 150));
checkOpacity(tester, flying(tester, find.text('Page 2')), 0.5555618554353714);
expect(
tester.getTopRight(flying(tester, find.text('Page 2'))),
const Offset(360.2321922779083, 13.5),
);
});
testWidgets('Top large title fades in and slides in from the right', (WidgetTester tester) async {
await startTransitionBetween(
tester,
to: const CupertinoSliverNavigationBar(),
toTitle: 'Page 2',
);
await tester.pump(const Duration(milliseconds: 50));
expect(flying(tester, find.text('Page 2')), findsOneWidget);
checkOpacity(tester, flying(tester, find.text('Page 2')), 0.0);
expect(
tester.getTopLeft(flying(tester, find.text('Page 2'))),
const Offset(781.625, 54.0),
);
await tester.pump(const Duration(milliseconds: 150));
checkOpacity(tester, flying(tester, find.text('Page 2')), 0.5292819738388062);
expect(
tester.getTopLeft(flying(tester, find.text('Page 2'))),
const Offset(195.53561544418335, 54.0),
);
});
testWidgets('Top large title fades in and slides in from the left in RTL', (WidgetTester tester) async {
await startTransitionBetween(
tester,
to: const CupertinoSliverNavigationBar(),
toTitle: 'Page 2',
textDirection: TextDirection.rtl,
);
await tester.pump(const Duration(milliseconds: 50));
expect(flying(tester, find.text('Page 2')), findsOneWidget);
checkOpacity(tester, flying(tester, find.text('Page 2')), 0.0);
expect(
tester.getTopRight(flying(tester, find.text('Page 2'))),
const Offset(18.375, 54.0),
);
await tester.pump(const Duration(milliseconds: 150));
checkOpacity(tester, flying(tester, find.text('Page 2')), 0.5292819738388062);
expect(
tester.getTopRight(flying(tester, find.text('Page 2'))),
const Offset(604.4643845558167, 54.0),
);
});
testWidgets('Components are not unnecessarily rebuilt during transitions', (WidgetTester tester) async {
int bottomBuildTimes = 0;
int topBuildTimes = 0;
await startTransitionBetween(
tester,
from: CupertinoNavigationBar(
middle: Builder(builder: (BuildContext context) {
bottomBuildTimes++;
return const Text('Page 1');
}),
),
to: CupertinoSliverNavigationBar(
largeTitle: Builder(builder: (BuildContext context) {
topBuildTimes++;
return const Text('Page 2');
}),
),
);
expect(bottomBuildTimes, 1);
// RenderSliverPersistentHeader.layoutChild causes 2 builds.
expect(topBuildTimes, 2);
await tester.pump();
// The shuttle builder builds the component widgets one more time.
expect(bottomBuildTimes, 2);
expect(topBuildTimes, 3);
// Subsequent animation needs to use reprojection of children.
await tester.pump();
expect(bottomBuildTimes, 2);
expect(topBuildTimes, 3);
await tester.pump(const Duration(milliseconds: 100));
expect(bottomBuildTimes, 2);
expect(topBuildTimes, 3);
// Finish animations.
await tester.pump(const Duration(milliseconds: 400));
expect(bottomBuildTimes, 2);
expect(topBuildTimes, 3);
});
testWidgets('Back swipe gesture transitions', (WidgetTester tester) async {
await startTransitionBetween(
tester,
fromTitle: 'Page 1',
toTitle: 'Page 2',
);
// Go to the next page.
await tester.pump(const Duration(milliseconds: 500));
// Start the gesture at the edge of the screen.
final TestGesture gesture = await tester.startGesture(const Offset(5.0, 200.0));
// Trigger the swipe.
await gesture.moveBy(const Offset(100.0, 0.0));
// Back gestures should trigger and draw the hero transition in the very same
// frame (since the "from" route has already moved to reveal the "to" route).
await tester.pump();
// Page 2, which is the middle of the top route, start to fly back to the right.
expect(
tester.getTopLeft(flying(tester, find.text('Page 2'))),
const Offset(353.5802058875561, 13.5),
);
// Page 1 is in transition in 2 places. Once as the top back label and once
// as the bottom middle.
expect(flying(tester, find.text('Page 1')), findsNWidgets(2));
// Past the halfway point now.
await gesture.moveBy(const Offset(500.0, 0.0));
await gesture.up();
await tester.pump();
// Transition continues.
expect(
tester.getTopLeft(flying(tester, find.text('Page 2'))),
const Offset(655.2055835723877, 13.5),
);
await tester.pump(const Duration(milliseconds: 50));
expect(
tester.getTopLeft(flying(tester, find.text('Page 2'))),
const Offset(749.6335566043854, 13.5),
);
await tester.pump(const Duration(milliseconds: 500));
// Cleans up properly
expect(() => flying(tester, find.text('Page 1')), throwsAssertionError);
expect(() => flying(tester, find.text('Page 2')), throwsAssertionError);
// Just the bottom route's middle now.
expect(find.text('Page 1'), findsOneWidget);
});
testWidgets('Back swipe gesture cancels properly with transition', (WidgetTester tester) async {
await startTransitionBetween(
tester,
fromTitle: 'Page 1',
toTitle: 'Page 2',
);
// Go to the next page.
await tester.pump(const Duration(milliseconds: 500));
// Start the gesture at the edge of the screen.
final TestGesture gesture = await tester.startGesture(const Offset(5.0, 200.0));
// Trigger the swipe.
await gesture.moveBy(const Offset(100.0, 0.0));
// Back gestures should trigger and draw the hero transition in the very same
// frame (since the "from" route has already moved to reveal the "to" route).
await tester.pump();
// Page 2, which is the middle of the top route, start to fly back to the right.
expect(
tester.getTopLeft(flying(tester, find.text('Page 2'))),
const Offset(353.5802058875561, 13.5),
);
await gesture.up();
await tester.pump();
// Transition continues from the point we let off.
expect(
tester.getTopLeft(flying(tester, find.text('Page 2'))),
const Offset(353.5802058875561, 13.5),
);
await tester.pump(const Duration(milliseconds: 50));
expect(
tester.getTopLeft(flying(tester, find.text('Page 2'))),
const Offset(350.0011436641216, 13.5),
);
// Finish the snap back animation.
await tester.pump(const Duration(milliseconds: 500));
// Cleans up properly
expect(() => flying(tester, find.text('Page 1')), throwsAssertionError);
expect(() => flying(tester, find.text('Page 2')), throwsAssertionError);
// Back to page 2.
expect(find.text('Page 2'), findsOneWidget);
});
}