blob: 6fd3add18a5cf49bff2d2749f6b3064266bcb9c2 [file]
// 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.
// This file is run as part of a reduced test set in CI on Mac and Windows
// machines.
@Tags(<String>['reduced-test-set'])
library;
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart' show DragStartBehavior;
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import '../rendering/rendering_tester.dart' show TestClipPaintingContext;
import 'semantics_tester.dart';
import 'states.dart';
import 'test_page_tester.dart';
void main() {
const kBlue = Color(0xFF0000FF);
const kGreen = Color(0xFF00FF00);
const kRed = Color(0xFFFF0000);
const kOrange = Color(0xFFFF8C00);
const kPurple = Color(0xFF800080);
const kYellow = Color(0xFFFFFF00);
// Regression test for https://github.com/flutter/flutter/issues/100451
testWidgets('PageView.builder respects findChildIndexCallback', (WidgetTester tester) async {
var finderCalled = false;
var itemCount = 7;
late StateSetter stateSetter;
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
stateSetter = setState;
return PageView.builder(
itemCount: itemCount,
itemBuilder: (BuildContext _, int index) =>
Container(key: Key('$index'), height: 2000.0),
findChildIndexCallback: (Key key) {
finderCalled = true;
return null;
},
);
},
),
),
);
expect(finderCalled, false);
// Trigger update.
stateSetter(() => itemCount = 77);
await tester.pump();
expect(finderCalled, true);
});
testWidgets('PageView resize from zero-size viewport should not lose state', (
WidgetTester tester,
) async {
// Regression test for https://github.com/flutter/flutter/issues/88956
final controller = PageController(initialPage: 1);
addTearDown(controller.dispose);
Widget build(Size size) {
return Directionality(
textDirection: TextDirection.ltr,
child: Center(
child: SizedBox.fromSize(
size: size,
child: PageView(
controller: controller,
onPageChanged: (int page) {},
children: kStates.map<Widget>((String state) => Text(state)).toList(),
),
),
),
);
}
// The pageView have a zero viewport, so nothing display.
await tester.pumpWidget(build(Size.zero));
expect(find.text('Alabama'), findsNothing);
expect(find.text('Alabama', skipOffstage: false), findsOneWidget);
// Resize from zero viewport to non-zero, the controller's initialPage 1 will display.
await tester.pumpWidget(build(const Size(200.0, 200.0)));
expect(find.text('Alaska'), findsOneWidget);
// Jump to page 'Iowa'.
controller.jumpToPage(kStates.indexOf('Iowa'));
await tester.pump();
expect(find.text('Iowa'), findsOneWidget);
// Resize to zero viewport again, nothing display.
await tester.pumpWidget(build(Size.zero));
expect(find.text('Iowa'), findsNothing);
// Resize from zero to non-zero, the pageView should not lose state, so the page 'Iowa' show again.
await tester.pumpWidget(build(const Size(200.0, 200.0)));
expect(find.text('Iowa'), findsOneWidget);
});
testWidgets('Change the page through the controller when zero-size viewport', (
WidgetTester tester,
) async {
// Regression test for https://github.com/flutter/flutter/issues/88956
// and https://github.com/flutter/flutter/issues/6537
final controller = PageController(initialPage: 1);
addTearDown(controller.dispose);
Widget build(Size size) {
return Directionality(
textDirection: TextDirection.ltr,
child: Center(
child: SizedBox.fromSize(
size: size,
child: PageView(
controller: controller,
onPageChanged: (int page) {},
children: kStates.map<Widget>((String state) => Text(state)).toList(),
),
),
),
);
}
// The pageView have a zero viewport, so nothing display.
await tester.pumpWidget(build(Size.zero));
expect(tester.getSize(find.byType(PageView)), Size.zero);
expect(find.text('Alabama'), findsNothing);
expect(find.text('Alabama', skipOffstage: false), findsOneWidget);
// Change the page through the page controller when zero viewport
controller.animateToPage(
kStates.indexOf('Iowa'),
duration: const Duration(milliseconds: 300),
curve: Curves.ease,
);
expect(controller.page, kStates.indexOf('Iowa'));
controller.jumpToPage(kStates.indexOf('Illinois'));
expect(controller.page, kStates.indexOf('Illinois'));
// Resize from zero viewport to non-zero, the latest state should not lost.
await tester.pumpWidget(build(const Size(200.0, 200.0)));
expect(controller.page, kStates.indexOf('Illinois'));
expect(find.text('Illinois'), findsOneWidget);
});
testWidgets('_PagePosition.applyViewportDimension should not throw', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/101007
final controller = PageController(initialPage: 1);
addTearDown(controller.dispose);
// Set the starting viewportDimension to 0.0
await tester.binding.setSurfaceSize(Size.zero);
final mediaQueryData = MediaQueryData.fromView(tester.view);
Widget build(Size size) {
return MediaQuery(
data: mediaQueryData.copyWith(size: size),
child: Directionality(
textDirection: TextDirection.ltr,
child: Center(
child: SizedBox.expand(
child: PageView(
controller: controller,
onPageChanged: (int page) {},
children: kStates.map<Widget>((String state) => Text(state)).toList(),
),
),
),
),
);
}
await tester.pumpWidget(build(Size.zero));
const surfaceSize = Size(500, 400);
await tester.binding.setSurfaceSize(surfaceSize);
await tester.pumpWidget(build(surfaceSize));
expect(tester.takeException(), isNull);
// Reset TestWidgetsFlutterBinding surfaceSize
await tester.binding.setSurfaceSize(null);
});
testWidgets('PageController cannot return page while unattached', (WidgetTester tester) async {
final controller = PageController();
addTearDown(controller.dispose);
expect(() => controller.page, throwsAssertionError);
});
testWidgets('PageView control test', (WidgetTester tester) async {
final log = <String>[];
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: PageView(
dragStartBehavior: DragStartBehavior.down,
children: kStates.map<Widget>((String state) {
return GestureDetector(
dragStartBehavior: DragStartBehavior.down,
onTap: () {
log.add(state);
},
child: Container(height: 200.0, color: kBlue, child: Text(state)),
);
}).toList(),
),
),
);
await tester.tap(find.text('Alabama'));
expect(log, equals(<String>['Alabama']));
log.clear();
expect(find.text('Alaska'), findsNothing);
await tester.drag(find.byType(PageView), const Offset(-20.0, 0.0));
await tester.pump();
expect(find.text('Alabama'), findsOneWidget);
expect(find.text('Alaska'), findsOneWidget);
expect(find.text('Arizona'), findsNothing);
await tester.pumpAndSettle();
expect(find.text('Alabama'), findsOneWidget);
expect(find.text('Alaska'), findsNothing);
await tester.drag(find.byType(PageView), const Offset(-401.0, 0.0));
await tester.pumpAndSettle();
expect(find.text('Alabama'), findsNothing);
expect(find.text('Alaska'), findsOneWidget);
expect(find.text('Arizona'), findsNothing);
await tester.tap(find.text('Alaska'));
expect(log, equals(<String>['Alaska']));
log.clear();
await tester.fling(find.byType(PageView), const Offset(-200.0, 0.0), 1000.0);
await tester.pumpAndSettle();
expect(find.text('Alabama'), findsNothing);
expect(find.text('Alaska'), findsNothing);
expect(find.text('Arizona'), findsOneWidget);
await tester.fling(find.byType(PageView), const Offset(200.0, 0.0), 1000.0);
await tester.pumpAndSettle();
expect(find.text('Alabama'), findsNothing);
expect(find.text('Alaska'), findsOneWidget);
expect(find.text('Arizona'), findsNothing);
});
testWidgets(
'PageView does not squish when overscrolled',
(WidgetTester tester) async {
await tester.pumpWidget(
TestWidgetsApp(
home: PageView(
children: List<Widget>.generate(10, (int i) {
return Container(key: ValueKey<int>(i), color: kBlue);
}),
),
),
);
Size sizeOf(int i) => tester.getSize(find.byKey(ValueKey<int>(i)));
double leftOf(int i) => tester.getTopLeft(find.byKey(ValueKey<int>(i))).dx;
expect(leftOf(0), equals(0.0));
expect(sizeOf(0), equals(const Size(800.0, 600.0)));
// Going into overscroll.
await tester.drag(find.byType(PageView), const Offset(100.0, 0.0));
await tester.pump();
expect(leftOf(0), greaterThan(0.0));
expect(sizeOf(0), equals(const Size(800.0, 600.0)));
// Easing overscroll past overscroll limit.
if (debugDefaultTargetPlatformOverride == TargetPlatform.macOS) {
await tester.drag(find.byType(PageView), const Offset(-500.0, 0.0));
} else {
await tester.drag(find.byType(PageView), const Offset(-200.0, 0.0));
}
await tester.pump();
expect(leftOf(0), lessThan(0.0));
expect(sizeOf(0), equals(const Size(800.0, 600.0)));
},
variant: const TargetPlatformVariant(<TargetPlatform>{
TargetPlatform.iOS,
TargetPlatform.macOS,
}),
);
testWidgets('PageController control test', (WidgetTester tester) async {
final controller = PageController(initialPage: 4);
addTearDown(controller.dispose);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Center(
child: SizedBox(
width: 600.0,
height: 400.0,
child: PageView(
controller: controller,
children: kStates.map<Widget>((String state) => Text(state)).toList(),
),
),
),
),
);
expect(find.text('California'), findsOneWidget);
controller.nextPage(duration: const Duration(milliseconds: 150), curve: Curves.ease);
await tester.pumpAndSettle();
expect(find.text('Colorado'), findsOneWidget);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Center(
child: SizedBox(
width: 300.0,
height: 400.0,
child: PageView(
controller: controller,
children: kStates.map<Widget>((String state) => Text(state)).toList(),
),
),
),
),
);
expect(find.text('Colorado'), findsOneWidget);
controller.previousPage(duration: const Duration(milliseconds: 150), curve: Curves.ease);
await tester.pumpAndSettle();
expect(find.text('California'), findsOneWidget);
});
testWidgets('PageController page stability', (WidgetTester tester) async {
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Center(
child: SizedBox(
width: 600.0,
height: 400.0,
child: PageView(children: kStates.map<Widget>((String state) => Text(state)).toList()),
),
),
),
);
expect(find.text('Alabama'), findsOneWidget);
await tester.drag(find.byType(PageView), const Offset(-1250.0, 0.0));
await tester.pumpAndSettle();
expect(find.text('Arizona'), findsOneWidget);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Center(
child: SizedBox(
width: 250.0,
height: 100.0,
child: PageView(children: kStates.map<Widget>((String state) => Text(state)).toList()),
),
),
),
);
expect(find.text('Arizona'), findsOneWidget);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Center(
child: SizedBox(
width: 450.0,
height: 400.0,
child: PageView(children: kStates.map<Widget>((String state) => Text(state)).toList()),
),
),
),
);
expect(find.text('Arizona'), findsOneWidget);
});
testWidgets('PageController nextPage and previousPage return Futures that resolve', (
WidgetTester tester,
) async {
final controller = PageController();
addTearDown(controller.dispose);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: PageView(
controller: controller,
children: kStates.map<Widget>((String state) => Text(state)).toList(),
),
),
);
var nextPageCompleted = false;
controller
.nextPage(duration: const Duration(milliseconds: 150), curve: Curves.ease)
.then((_) => nextPageCompleted = true);
expect(nextPageCompleted, false);
await tester.pump(const Duration(milliseconds: 200));
expect(nextPageCompleted, false);
await tester.pump(const Duration(milliseconds: 200));
expect(nextPageCompleted, true);
var previousPageCompleted = false;
controller
.previousPage(duration: const Duration(milliseconds: 150), curve: Curves.ease)
.then((_) => previousPageCompleted = true);
expect(previousPageCompleted, false);
await tester.pump(const Duration(milliseconds: 200));
expect(previousPageCompleted, false);
await tester.pump(const Duration(milliseconds: 200));
expect(previousPageCompleted, true);
});
testWidgets('PageView in zero-size container', (WidgetTester tester) async {
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Center(
child: SizedBox.shrink(
child: PageView(children: kStates.map<Widget>((String state) => Text(state)).toList()),
),
),
),
);
expect(find.text('Alabama', skipOffstage: false), findsOneWidget);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Center(
child: SizedBox.square(
dimension: 200.0,
child: PageView(children: kStates.map<Widget>((String state) => Text(state)).toList()),
),
),
),
);
expect(find.text('Alabama'), findsOneWidget);
});
testWidgets('Page changes at halfway point', (WidgetTester tester) async {
final log = <int>[];
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: PageView(
onPageChanged: log.add,
children: kStates.map<Widget>((String state) => Text(state)).toList(),
),
),
);
expect(log, isEmpty);
final TestGesture gesture = await tester.startGesture(const Offset(100.0, 100.0));
// The page view is 800.0 wide, so this move is just short of halfway.
await gesture.moveBy(const Offset(-380.0, 0.0));
expect(log, isEmpty);
// We've crossed the halfway mark.
await gesture.moveBy(const Offset(-40.0, 0.0));
expect(log, equals(const <int>[1]));
log.clear();
// Moving a bit more should not generate redundant notifications.
await gesture.moveBy(const Offset(-40.0, 0.0));
expect(log, isEmpty);
await gesture.moveBy(const Offset(-40.0, 0.0));
await tester.pump();
await gesture.moveBy(const Offset(-40.0, 0.0));
await tester.pump();
await gesture.moveBy(const Offset(-40.0, 0.0));
await tester.pump();
expect(log, isEmpty);
await gesture.up();
await tester.pumpAndSettle();
expect(log, isEmpty);
expect(find.text('Alabama'), findsNothing);
expect(find.text('Alaska'), findsOneWidget);
});
testWidgets('Bouncing scroll physics ballistics does not overshoot', (WidgetTester tester) async {
final log = <int>[];
final controller = PageController(viewportFraction: 0.9);
addTearDown(controller.dispose);
Widget build(PageController controller, {Size? size}) {
final Widget pageView = Directionality(
textDirection: TextDirection.ltr,
child: PageView(
controller: controller,
onPageChanged: log.add,
physics: const BouncingScrollPhysics(),
children: kStates.map<Widget>((String state) => Text(state)).toList(),
),
);
if (size != null) {
return OverflowBox(
minWidth: size.width,
minHeight: size.height,
maxWidth: size.width,
maxHeight: size.height,
child: pageView,
);
} else {
return pageView;
}
}
await tester.pumpWidget(build(controller));
expect(log, isEmpty);
// Fling right to move to a non-existent page at the beginning of the
// PageView, and confirm that the PageView settles back on the first page.
await tester.fling(find.byType(PageView), const Offset(100.0, 0.0), 800.0);
await tester.pumpAndSettle();
expect(log, isEmpty);
expect(find.text('Alabama'), findsOneWidget);
expect(find.text('Alaska'), findsOneWidget);
expect(find.text('Arizona'), findsNothing);
// Try again with a Cupertino "Plus" device size.
await tester.pumpWidget(build(controller, size: const Size(414.0, 736.0)));
expect(log, isEmpty);
await tester.fling(find.byType(PageView), const Offset(100.0, 0.0), 800.0);
await tester.pumpAndSettle();
expect(log, isEmpty);
expect(find.text('Alabama'), findsOneWidget);
expect(find.text('Alaska'), findsOneWidget);
expect(find.text('Arizona'), findsNothing);
});
testWidgets('PageView viewportFraction', (WidgetTester tester) async {
var controller = PageController(viewportFraction: 7 / 8);
addTearDown(controller.dispose);
Widget build(PageController controller) {
return Directionality(
textDirection: TextDirection.ltr,
child: PageView.builder(
controller: controller,
itemCount: kStates.length,
itemBuilder: (BuildContext context, int index) {
return Container(
height: 200.0,
color: index.isEven ? kBlue : kGreen,
child: Text(kStates[index]),
);
},
),
);
}
await tester.pumpWidget(build(controller));
expect(tester.getTopLeft(find.text('Alabama')), const Offset(50.0, 0.0));
expect(tester.getTopLeft(find.text('Alaska')), const Offset(750.0, 0.0));
controller.jumpToPage(10);
await tester.pump();
expect(tester.getTopLeft(find.text('Georgia')), const Offset(-650.0, 0.0));
expect(tester.getTopLeft(find.text('Hawaii')), const Offset(50.0, 0.0));
expect(tester.getTopLeft(find.text('Idaho')), const Offset(750.0, 0.0));
controller = PageController(viewportFraction: 39 / 40);
addTearDown(controller.dispose);
await tester.pumpWidget(build(controller));
expect(tester.getTopLeft(find.text('Georgia')), const Offset(-770.0, 0.0));
expect(tester.getTopLeft(find.text('Hawaii')), const Offset(10.0, 0.0));
expect(tester.getTopLeft(find.text('Idaho')), const Offset(790.0, 0.0));
});
testWidgets('Page snapping disable and reenable', (WidgetTester tester) async {
final log = <int>[];
Widget build({required bool pageSnapping}) {
return Directionality(
textDirection: TextDirection.ltr,
child: PageView(
pageSnapping: pageSnapping,
onPageChanged: log.add,
children: kStates.map<Widget>((String state) => Text(state)).toList(),
),
);
}
await tester.pumpWidget(build(pageSnapping: true));
expect(log, isEmpty);
// Drag more than halfway to the next page, to confirm the default behavior.
TestGesture gesture = await tester.startGesture(const Offset(100.0, 100.0));
// The page view is 800.0 wide, so this move is just beyond halfway.
await gesture.moveBy(const Offset(-420.0, 0.0));
expect(log, equals(const <int>[1]));
log.clear();
// Release the gesture, confirm that the page settles on the next.
await gesture.up();
await tester.pumpAndSettle();
expect(find.text('Alabama'), findsNothing);
expect(find.text('Alaska'), findsOneWidget);
// Disable page snapping, and try moving halfway. Confirm it doesn't snap.
await tester.pumpWidget(build(pageSnapping: false));
gesture = await tester.startGesture(const Offset(100.0, 100.0));
// Move just beyond halfway, again.
await gesture.moveBy(const Offset(-420.0, 0.0));
// Page notifications still get sent.
expect(log, equals(const <int>[2]));
log.clear();
// Release the gesture, confirm that both pages are visible.
await gesture.up();
await tester.pumpAndSettle();
expect(find.text('Alabama'), findsNothing);
expect(find.text('Alaska'), findsOneWidget);
expect(find.text('Arizona'), findsOneWidget);
expect(find.text('Arkansas'), findsNothing);
// Now re-enable snapping, confirm that we've settled on a page.
await tester.pumpWidget(build(pageSnapping: true));
await tester.pumpAndSettle();
expect(log, isEmpty);
expect(find.text('Alaska'), findsNothing);
expect(find.text('Arizona'), findsOneWidget);
expect(find.text('Arkansas'), findsNothing);
});
testWidgets('PageView small viewportFraction', (WidgetTester tester) async {
final controller = PageController(viewportFraction: 1 / 8);
addTearDown(controller.dispose);
Widget build(PageController controller) {
return Directionality(
textDirection: TextDirection.ltr,
child: PageView.builder(
controller: controller,
itemCount: kStates.length,
itemBuilder: (BuildContext context, int index) {
return Container(
height: 200.0,
color: index.isEven ? kBlue : kGreen,
child: Text(kStates[index]),
);
},
),
);
}
await tester.pumpWidget(build(controller));
expect(tester.getTopLeft(find.text('Alabama')), const Offset(350.0, 0.0));
expect(tester.getTopLeft(find.text('Alaska')), const Offset(450.0, 0.0));
expect(tester.getTopLeft(find.text('Arizona')), const Offset(550.0, 0.0));
expect(tester.getTopLeft(find.text('Arkansas')), const Offset(650.0, 0.0));
expect(tester.getTopLeft(find.text('California')), const Offset(750.0, 0.0));
controller.jumpToPage(10);
await tester.pump();
expect(tester.getTopLeft(find.text('Connecticut')), const Offset(-50.0, 0.0));
expect(tester.getTopLeft(find.text('Delaware')), const Offset(50.0, 0.0));
expect(tester.getTopLeft(find.text('Florida')), const Offset(150.0, 0.0));
expect(tester.getTopLeft(find.text('Georgia')), const Offset(250.0, 0.0));
expect(tester.getTopLeft(find.text('Hawaii')), const Offset(350.0, 0.0));
expect(tester.getTopLeft(find.text('Idaho')), const Offset(450.0, 0.0));
expect(tester.getTopLeft(find.text('Illinois')), const Offset(550.0, 0.0));
expect(tester.getTopLeft(find.text('Indiana')), const Offset(650.0, 0.0));
expect(tester.getTopLeft(find.text('Iowa')), const Offset(750.0, 0.0));
});
testWidgets('PageView large viewportFraction', (WidgetTester tester) async {
final controller = PageController(viewportFraction: 5 / 4);
addTearDown(controller.dispose);
Widget build(PageController controller) {
return Directionality(
textDirection: TextDirection.ltr,
child: PageView.builder(
controller: controller,
itemCount: kStates.length,
itemBuilder: (BuildContext context, int index) {
return Container(
height: 200.0,
color: index.isEven ? kBlue : kGreen,
child: Text(kStates[index]),
);
},
),
);
}
await tester.pumpWidget(build(controller));
expect(tester.getTopLeft(find.text('Alabama')), const Offset(-100.0, 0.0));
expect(tester.getBottomRight(find.text('Alabama')), const Offset(900.0, 600.0));
controller.jumpToPage(10);
await tester.pump();
expect(tester.getTopLeft(find.text('Hawaii')), const Offset(-100.0, 0.0));
});
testWidgets('Updating PageView large viewportFraction', (WidgetTester tester) async {
Widget build(PageController controller) {
return Directionality(
textDirection: TextDirection.ltr,
child: PageView.builder(
controller: controller,
itemCount: kStates.length,
itemBuilder: (BuildContext context, int index) {
return Container(
height: 200.0,
color: index.isEven ? kBlue : kGreen,
child: Text(kStates[index]),
);
},
),
);
}
final oldController = PageController(viewportFraction: 5 / 4);
addTearDown(oldController.dispose);
await tester.pumpWidget(build(oldController));
expect(tester.getTopLeft(find.text('Alabama')), const Offset(-100, 0));
expect(tester.getBottomRight(find.text('Alabama')), const Offset(900.0, 600.0));
final newController = PageController(viewportFraction: 4);
addTearDown(newController.dispose);
await tester.pumpWidget(build(newController));
newController.jumpToPage(10);
await tester.pump();
expect(tester.getTopLeft(find.text('Hawaii')), const Offset(-(4 - 1) * 800 / 2, 0));
});
testWidgets('PageView large viewportFraction can scroll to the last page and snap', (
WidgetTester tester,
) async {
// Regression test for https://github.com/flutter/flutter/issues/45096.
final controller = PageController(viewportFraction: 5 / 4);
addTearDown(controller.dispose);
Widget build(PageController controller) {
return Directionality(
textDirection: TextDirection.ltr,
child: PageView.builder(
controller: controller,
itemCount: 3,
itemBuilder: (BuildContext context, int index) {
return Container(
height: 200.0,
color: index.isEven ? kBlue : kGreen,
child: Text(index.toString()),
);
},
),
);
}
await tester.pumpWidget(build(controller));
expect(tester.getCenter(find.text('0')), const Offset(400, 300));
controller.jumpToPage(2);
await tester.pump();
await tester.pumpAndSettle();
expect(tester.getCenter(find.text('2')), const Offset(400, 300));
});
testWidgets('All visible pages are able to receive touch events', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/23873.
final controller = PageController(viewportFraction: 1 / 4);
addTearDown(controller.dispose);
late int tappedIndex;
Widget build() {
return Directionality(
textDirection: TextDirection.ltr,
child: PageView.builder(
controller: controller,
itemCount: 20,
itemBuilder: (BuildContext context, int index) {
return GestureDetector(
onTap: () => tappedIndex = index,
child: SizedBox.expand(child: Text('$index')),
);
},
),
);
}
Iterable<int> visiblePages = const <int>[0, 1, 2];
await tester.pumpWidget(build());
// The first 3 items should be visible and tappable.
for (final index in visiblePages) {
expect(find.text(index.toString()), findsOneWidget);
// The center of page 2's x-coordinate is 800, so we have to manually
// offset it a bit to make sure the tap lands within the screen.
final Offset center = tester.getCenter(find.text('$index')) - const Offset(3, 0);
await tester.tapAt(center);
expect(tappedIndex, index);
}
controller.jumpToPage(19);
await tester.pump();
// The last 3 items should be visible and tappable.
visiblePages = const <int>[17, 18, 19];
for (final index in visiblePages) {
expect(find.text('$index'), findsOneWidget);
await tester.tap(find.text('$index'));
expect(tappedIndex, index);
}
});
testWidgets('the current item remains centered on constraint change', (
WidgetTester tester,
) async {
// Regression test for https://github.com/flutter/flutter/issues/50505.
final controller = PageController(initialPage: kStates.length - 1, viewportFraction: 0.5);
addTearDown(controller.dispose);
Widget build(Size size) {
return Directionality(
textDirection: TextDirection.ltr,
child: Center(
child: SizedBox.fromSize(
size: size,
child: PageView(
controller: controller,
children: kStates.map<Widget>((String state) => Text(state)).toList(),
onPageChanged: (int page) {},
),
),
),
);
}
// Verifies that the last item is centered on screen.
void verifyCentered() {
expect(
tester.getCenter(find.text(kStates.last)),
offsetMoreOrLessEquals(const Offset(400, 300)),
);
}
await tester.pumpWidget(build(const Size(300, 300)));
await tester.pumpAndSettle();
verifyCentered();
await tester.pumpWidget(build(const Size(200, 300)));
await tester.pumpAndSettle();
verifyCentered();
});
testWidgets('PageView does not report page changed on overscroll', (WidgetTester tester) async {
final controller = PageController(initialPage: kStates.length - 1);
addTearDown(controller.dispose);
var changeIndex = 0;
Widget build() {
return Directionality(
textDirection: TextDirection.ltr,
child: PageView(
controller: controller,
children: kStates.map<Widget>((String state) => Text(state)).toList(),
onPageChanged: (int page) {
changeIndex = page;
},
),
);
}
await tester.pumpWidget(build());
controller.jumpToPage(kStates.length * 2); // try to move beyond max range
// change index should be zero, shouldn't fire onPageChanged
expect(changeIndex, 0);
await tester.pump();
expect(changeIndex, 0);
});
testWidgets('PageView can restore page', (WidgetTester tester) async {
final controller = PageController();
addTearDown(controller.dispose);
expect(
() => controller.page,
throwsA(
isAssertionError.having(
(AssertionError error) => error.message,
'message',
equals('PageController.page cannot be accessed before a PageView is built with it.'),
),
),
);
final bucket = PageStorageBucket();
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: PageStorage(
bucket: bucket,
child: PageView(
key: const PageStorageKey<String>('PageView'),
controller: controller,
children: const <Widget>[Placeholder(), Placeholder(), Placeholder()],
),
),
),
);
expect(controller.page, 0);
controller.jumpToPage(2);
expect(await tester.pumpAndSettle(const Duration(minutes: 1)), 2);
expect(controller.page, 2);
await tester.pumpWidget(PageStorage(bucket: bucket, child: Container()));
expect(
() => controller.page,
throwsA(
isAssertionError.having(
(AssertionError error) => error.message,
'message',
equals('PageController.page cannot be accessed before a PageView is built with it.'),
),
),
);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: PageStorage(
bucket: bucket,
child: PageView(
key: const PageStorageKey<String>('PageView'),
controller: controller,
children: const <Widget>[Placeholder(), Placeholder(), Placeholder()],
),
),
),
);
expect(controller.page, 2);
final controller2 = PageController(keepPage: false);
addTearDown(controller2.dispose);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: PageStorage(
bucket: bucket,
child: PageView(
key: const PageStorageKey<String>(
'Check it again against your list and see consistency!',
),
controller: controller2,
children: const <Widget>[Placeholder(), Placeholder(), Placeholder()],
),
),
),
);
expect(controller2.page, 0);
});
testWidgets('PageView exposes semantics of children', (WidgetTester tester) async {
final semantics = SemanticsTester(tester);
final controller = PageController();
addTearDown(controller.dispose);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: PageView(
controller: controller,
children: List<Widget>.generate(3, (int i) {
return Semantics(container: true, child: Text('Page #$i'));
}),
),
),
);
expect(controller.page, 0);
expect(semantics, includesNodeWith(label: 'Page #0'));
expect(semantics, isNot(includesNodeWith(label: 'Page #1')));
expect(semantics, isNot(includesNodeWith(label: 'Page #2')));
controller.jumpToPage(1);
await tester.pumpAndSettle();
expect(semantics, isNot(includesNodeWith(label: 'Page #0')));
expect(semantics, includesNodeWith(label: 'Page #1'));
expect(semantics, isNot(includesNodeWith(label: 'Page #2')));
controller.jumpToPage(2);
await tester.pumpAndSettle();
expect(semantics, isNot(includesNodeWith(label: 'Page #0')));
expect(semantics, isNot(includesNodeWith(label: 'Page #1')));
expect(semantics, includesNodeWith(label: 'Page #2'));
semantics.dispose();
});
testWidgets('PageMetrics', (WidgetTester tester) async {
final page = PageMetrics(
minScrollExtent: 100.0,
maxScrollExtent: 200.0,
pixels: 150.0,
viewportDimension: 25.0,
axisDirection: AxisDirection.right,
viewportFraction: 1.0,
devicePixelRatio: tester.view.devicePixelRatio,
);
expect(page.page, 6);
final PageMetrics page2 = page.copyWith(pixels: page.pixels - 100.0);
expect(page2.page, 4.0);
});
testWidgets('Page controller can handle rounding issue', (WidgetTester tester) async {
final pageController = PageController();
addTearDown(pageController.dispose);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: PageView(
controller: pageController,
children: List<Widget>.generate(3, (int i) {
return Semantics(container: true, child: Text('Page #$i'));
}),
),
),
);
// Simulate precision error.
pageController.position.jumpTo(799.99999999999);
expect(pageController.page, 1);
});
testWidgets('PageView can participate in a11y scrolling', (WidgetTester tester) async {
final semantics = SemanticsTester(tester);
final controller = PageController();
addTearDown(controller.dispose);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: PageView(
controller: controller,
allowImplicitScrolling: true,
children: List<Widget>.generate(4, (int i) {
return Semantics(container: true, child: Text('Page #$i'));
}),
),
),
);
expect(controller.page, 0);
expect(semantics, includesNodeWith(flags: <SemanticsFlag>[SemanticsFlag.hasImplicitScrolling]));
expect(semantics, includesNodeWith(label: 'Page #0'));
expect(
semantics,
includesNodeWith(label: 'Page #1', flags: <SemanticsFlag>[SemanticsFlag.isHidden]),
);
expect(
semantics,
isNot(includesNodeWith(label: 'Page #2', flags: <SemanticsFlag>[SemanticsFlag.isHidden])),
);
expect(
semantics,
isNot(includesNodeWith(label: 'Page #3', flags: <SemanticsFlag>[SemanticsFlag.isHidden])),
);
controller.nextPage(duration: const Duration(milliseconds: 150), curve: Curves.ease);
await tester.pumpAndSettle();
expect(
semantics,
includesNodeWith(label: 'Page #0', flags: <SemanticsFlag>[SemanticsFlag.isHidden]),
);
expect(semantics, includesNodeWith(label: 'Page #1'));
expect(
semantics,
includesNodeWith(label: 'Page #2', flags: <SemanticsFlag>[SemanticsFlag.isHidden]),
);
expect(
semantics,
isNot(includesNodeWith(label: 'Page #3', flags: <SemanticsFlag>[SemanticsFlag.isHidden])),
);
controller.nextPage(duration: const Duration(milliseconds: 150), curve: Curves.ease);
await tester.pumpAndSettle();
expect(
semantics,
isNot(includesNodeWith(label: 'Page #0', flags: <SemanticsFlag>[SemanticsFlag.isHidden])),
);
expect(
semantics,
includesNodeWith(label: 'Page #1', flags: <SemanticsFlag>[SemanticsFlag.isHidden]),
);
expect(semantics, includesNodeWith(label: 'Page #2'));
expect(
semantics,
includesNodeWith(label: 'Page #3', flags: <SemanticsFlag>[SemanticsFlag.isHidden]),
);
semantics.dispose();
});
testWidgets('PageView respects clipBehavior', (WidgetTester tester) async {
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: PageView(children: <Widget>[Container(height: 2000.0)]),
),
);
// 1st, check that the render object has received the default clip behavior.
final RenderViewport renderObject = tester.allRenderObjects.whereType<RenderViewport>().first;
expect(renderObject.clipBehavior, equals(Clip.hardEdge));
// 2nd, check that the painting context has received the default clip behavior.
final context = TestClipPaintingContext();
renderObject.paint(context, Offset.zero);
expect(context.clipBehavior, equals(Clip.hardEdge));
// 3rd, pump a new widget to check that the render object can update its clip behavior.
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: PageView(
clipBehavior: Clip.antiAlias,
children: <Widget>[Container(height: 2000.0)],
),
),
);
expect(renderObject.clipBehavior, equals(Clip.antiAlias));
// 4th, check that a non-default clip behavior can be sent to the painting context.
renderObject.paint(context, Offset.zero);
expect(context.clipBehavior, equals(Clip.antiAlias));
});
testWidgets('PageView.padEnds tests', (WidgetTester tester) async {
Finder viewportFinder() => find.byType(SliverFillViewport, skipOffstage: false);
// PageView() defaults to true.
await tester.pumpWidget(Directionality(textDirection: TextDirection.ltr, child: PageView()));
expect(tester.widget<SliverFillViewport>(viewportFinder()).padEnds, true);
// PageView(padEnds: false) is propagated properly.
await tester.pumpWidget(
Directionality(textDirection: TextDirection.ltr, child: PageView(padEnds: false)),
);
expect(tester.widget<SliverFillViewport>(viewportFinder()).padEnds, false);
});
testWidgets('PageView - precision error inside RenderSliverFixedExtentBoxAdaptor', (
WidgetTester tester,
) async {
// Regression test for https://github.com/flutter/flutter/issues/95101
final controller = PageController(initialPage: 152);
addTearDown(controller.dispose);
await tester.pumpWidget(
Center(
child: SizedBox(
width: 392.72727272727275,
child: Directionality(
textDirection: TextDirection.ltr,
child: PageView.builder(
controller: controller,
itemCount: 366,
itemBuilder: (BuildContext context, int index) {
return const SizedBox();
},
),
),
),
),
);
controller.jumpToPage(365);
await tester.pump();
expect(tester.takeException(), isNull);
});
testWidgets('PageView content should not be stretched on precision error', (
WidgetTester tester,
) async {
// Regression test for https://github.com/flutter/flutter/issues/126561.
final controller = PageController();
addTearDown(controller.dispose);
const pixel6EmulatorWidth = 411.42857142857144;
const errorTextStyle = TextStyle(
color: Color(0xD0FF0000),
fontFamily: 'monospace',
fontSize: 48.0,
fontWeight: FontWeight.w900,
decoration: TextDecoration.underline,
decorationColor: Color(0xFFFFFF00),
decorationStyle: TextDecorationStyle.double,
);
await tester.pumpWidget(
TestWidgetsApp(
textStyle: errorTextStyle,
home: ScrollConfiguration(
behavior: const ScrollBehavior().copyWith(overscroll: false),
child: Center(
child: SizedBox(
width: pixel6EmulatorWidth,
child: PageView(
controller: controller,
physics: const PageScrollPhysics().applyTo(const ClampingScrollPhysics()),
children: const <Widget>[
Center(child: Text('First Page')),
Center(child: Text('Second Page')),
Center(child: Text('Third Page')),
],
),
),
),
),
),
);
controller.animateToPage(2, duration: const Duration(milliseconds: 300), curve: Curves.ease);
await tester.pumpAndSettle();
await expectLater(
find.byType(PageView),
matchesGoldenFile('page_view_no_stretch_precision_error.png'),
);
});
testWidgets('PageController onAttach, onDetach', (WidgetTester tester) async {
var attach = 0;
var detach = 0;
final controller = PageController(
onAttach: (_) {
attach++;
},
onDetach: (_) {
detach++;
},
);
addTearDown(controller.dispose);
await tester.pumpWidget(
TestWidgetsApp(
home: Center(
child: PageView(
controller: controller,
physics: const PageScrollPhysics().applyTo(const ClampingScrollPhysics()),
children: const <Widget>[
Center(child: Text('First Page')),
Center(child: Text('Second Page')),
Center(child: Text('Third Page')),
],
),
),
),
);
await tester.pumpAndSettle();
expect(attach, 1);
expect(detach, 0);
await tester.pumpWidget(Container());
await tester.pumpAndSettle();
expect(attach, 1);
expect(detach, 1);
});
group('$PageView handles change of controller', () {
final GlobalKey key = GlobalKey();
Widget createPageView(PageController? controller) {
return TestWidgetsApp(
home: PageView(
key: key,
controller: controller,
children: const <Widget>[
Center(child: Text('0')),
Center(child: Text('1')),
Center(child: Text('2')),
],
),
);
}
Future<void> testPageViewWithController(
PageController controller,
WidgetTester tester,
bool controls,
) async {
int currentVisiblePage() {
return int.parse(tester.widgetList(find.byType(Text)).whereType<Text>().first.data!);
}
final int initialPageInView = currentVisiblePage();
for (var i = 0; i < 3; i++) {
if (controls) {
controller.jumpToPage(i);
await tester.pumpAndSettle();
expect(currentVisiblePage(), i);
} else {
expect(() => controller.jumpToPage(i), throwsAssertionError);
expect(currentVisiblePage(), initialPageInView);
}
}
}
testWidgets('null to value', (WidgetTester tester) async {
final controller = PageController();
addTearDown(controller.dispose);
await tester.pumpWidget(createPageView(null));
await tester.pumpWidget(createPageView(controller));
await testPageViewWithController(controller, tester, true);
});
testWidgets('value to value', (WidgetTester tester) async {
final controller1 = PageController();
addTearDown(controller1.dispose);
final controller2 = PageController();
addTearDown(controller2.dispose);
await tester.pumpWidget(createPageView(controller1));
await testPageViewWithController(controller1, tester, true);
await tester.pumpWidget(createPageView(controller2));
await testPageViewWithController(controller1, tester, false);
await testPageViewWithController(controller2, tester, true);
});
testWidgets('value to null', (WidgetTester tester) async {
final controller = PageController();
addTearDown(controller.dispose);
await tester.pumpWidget(createPageView(controller));
await testPageViewWithController(controller, tester, true);
await tester.pumpWidget(createPageView(null));
await testPageViewWithController(controller, tester, false);
});
testWidgets('null to null', (WidgetTester tester) async {
await tester.pumpWidget(createPageView(null));
await tester.pumpWidget(createPageView(null));
});
});
group('Asserts in jumpToPage and animateToPage methods works properly', () {
Widget createPageView([PageController? controller]) {
return TestWidgetsApp(
home: PageView(
controller: controller,
children: <Widget>[
Container(color: kRed),
Container(color: kGreen),
Container(color: kBlue),
],
),
);
}
group('One pageController is attached to multiple PageViews', () {
Widget createMultiplePageViews(PageController controller) {
return TestWidgetsApp(
home: Column(
children: <Widget>[
Expanded(
child: PageView(
controller: controller,
children: <Widget>[
Container(color: kRed),
Container(color: kGreen),
Container(color: kBlue),
],
),
),
Expanded(
child: PageView(
controller: controller,
children: <Widget>[
Container(color: kOrange),
Container(color: kPurple),
Container(color: kYellow),
],
),
),
],
),
);
}
testWidgets(
'animateToPage assertion is working properly when pageController is attached to multiple PageViews',
(WidgetTester tester) async {
final controller = PageController();
addTearDown(controller.dispose);
await tester.pumpWidget(createMultiplePageViews(controller));
expect(
() => controller.animateToPage(
2,
duration: const Duration(milliseconds: 300),
curve: Curves.ease,
),
throwsA(
isAssertionError.having(
(AssertionError error) => error.message,
'message',
equals(
'Multiple PageViews are attached to '
'the same PageController.',
),
),
),
);
},
);
testWidgets(
'jumpToPage assertion is working properly when pageController is attached to multiple PageViews',
(WidgetTester tester) async {
final controller = PageController();
addTearDown(controller.dispose);
await tester.pumpWidget(createMultiplePageViews(controller));
expect(
() => controller.jumpToPage(2),
throwsA(
isAssertionError.having(
(AssertionError error) => error.message,
'message',
equals(
'Multiple PageViews are attached to '
'the same PageController.',
),
),
),
);
},
);
});
group('PageController is attached or is not attached to PageView', () {
testWidgets('Assert behavior of animateToPage works properly', (WidgetTester tester) async {
final controller = PageController();
addTearDown(controller.dispose);
// pageController is not attached to PageView
await tester.pumpWidget(createPageView());
expect(
() => controller.animateToPage(
2,
duration: const Duration(milliseconds: 300),
curve: Curves.ease,
),
throwsA(
isAssertionError.having(
(AssertionError error) => error.message,
'message',
equals('PageController is not attached to a PageView.'),
),
),
);
// pageController is attached to PageView
await tester.pumpWidget(createPageView(controller));
expect(
() => controller.animateToPage(
2,
duration: const Duration(milliseconds: 300),
curve: Curves.ease,
),
returnsNormally,
);
});
testWidgets('Assert behavior of jumpToPage works properly', (WidgetTester tester) async {
final controller = PageController();
addTearDown(controller.dispose);
// pageController is not attached to PageView
await tester.pumpWidget(createPageView());
expect(
() => controller.jumpToPage(2),
throwsA(
isAssertionError.having(
(AssertionError error) => error.message,
'message',
equals('PageController is not attached to a PageView.'),
),
),
);
// pageController is attached to PageView
await tester.pumpWidget(createPageView(controller));
expect(() => controller.jumpToPage(2), returnsNormally);
});
});
});
testWidgets(
'Get the page value before the content dimension is determined,do not throw an assertion and return null',
(WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/146986.
final controller = PageController();
late String currentPage;
addTearDown(controller.dispose);
await tester.pumpWidget(
TestWidgetsApp(
home: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Column(
children: <Widget>[
Expanded(
child: PageView(
controller: controller,
children: <Widget>[
Builder(
builder: (BuildContext context) {
currentPage = controller.page == null ? 'null' : 'not empty';
return Center(child: Text(currentPage));
},
),
],
),
),
GestureDetector(
key: const Key('rebuild-trigger'),
onTap: () {
setState(() {});
},
child: const Text('Tap'),
),
],
);
},
),
),
);
expect(find.text('null'), findsOneWidget);
expect(find.text('not empty'), findsNothing);
expect(currentPage, 'null');
await tester.tap(find.byKey(const Key('rebuild-trigger')));
await tester.pump();
currentPage = controller.page == null ? 'null' : 'not empty';
expect(find.text('not empty'), findsOneWidget);
expect(find.text('null'), findsNothing);
expect(currentPage, 'not empty');
},
);
testWidgets('Does not crash when calling jumpToPage before layout', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/86222.
final controller = PageController();
addTearDown(controller.dispose);
await tester.pumpWidget(
TestWidgetsApp(
home: Navigator(
onDidRemovePage: (Page<Object?> page) {},
pages: <Page<void>>[
TestPage<void>(
child: PageView(
controller: controller,
children: const <Widget>[Text('One'), Text('Two')],
),
),
const TestPage<void>(child: SizedBox.shrink()),
],
),
),
);
controller.jumpToPage(1);
expect(tester.takeException(), null);
});
testWidgets('Does not crash when calling animateToPage before layout', (
WidgetTester tester,
) async {
// Regression test for https://github.com/flutter/flutter/issues/86222.
final controller = PageController();
addTearDown(controller.dispose);
await tester.pumpWidget(
TestWidgetsApp(
home: Navigator(
onDidRemovePage: (Page<Object?> page) {},
pages: <Page<void>>[
TestPage<void>(
child: PageView(
controller: controller,
children: const <Widget>[Text('One'), Text('Two')],
),
),
const TestPage<void>(child: SizedBox.shrink()),
],
),
),
);
controller.animateToPage(1, duration: const Duration(milliseconds: 50), curve: Curves.bounceIn);
expect(tester.takeException(), null);
});
testWidgets('PageView respects scrollCacheExtent', (WidgetTester tester) async {
final controller = PageController();
addTearDown(controller.dispose);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: PageView(
controller: controller,
allowImplicitScrolling: true,
scrollCacheExtent: const ScrollCacheExtent.viewport(1.0),
children: List<Widget>.generate(3, (int i) {
return SizedBox(key: ValueKey<int>(i), child: Text('Page $i'));
}),
),
),
);
expect(find.text('Page 0'), findsOneWidget);
expect(find.text('Page 1', skipOffstage: false), findsOneWidget);
expect(find.text('Page 2', skipOffstage: false), findsNothing);
});
testWidgets('PageView scrollCacheExtent defaults to allowImplicitScrolling behavior', (
WidgetTester tester,
) async {
final controller = PageController();
addTearDown(controller.dispose);
Widget build({required bool allowImplicitScrolling, ScrollCacheExtent? scrollCacheExtent}) {
return Directionality(
textDirection: TextDirection.ltr,
child: PageView(
controller: controller,
allowImplicitScrolling: allowImplicitScrolling,
scrollCacheExtent: scrollCacheExtent,
children: List<Widget>.generate(3, (int i) {
return SizedBox(key: ValueKey<int>(i), child: Text('Page $i'));
}),
),
);
}
// Default (allowImplicitScrolling: false) -> scrollCacheExtent: viewport(0.0)
await tester.pumpWidget(build(allowImplicitScrolling: false));
expect(find.text('Page 1', skipOffstage: false), findsNothing);
// allowImplicitScrolling: true -> scrollCacheExtent: viewport(1.0)
await tester.pumpWidget(build(allowImplicitScrolling: true));
expect(find.text('Page 1', skipOffstage: false), findsOneWidget);
});
testWidgets('PageView updates scrollCacheExtent dynamically', (WidgetTester tester) async {
final controller = PageController();
addTearDown(controller.dispose);
Widget build({required ScrollCacheExtent scrollCacheExtent}) {
return Directionality(
textDirection: TextDirection.ltr,
child: PageView(
controller: controller,
allowImplicitScrolling: true,
scrollCacheExtent: scrollCacheExtent,
children: List<Widget>.generate(3, (int i) {
return SizedBox(key: ValueKey<int>(i), child: Text('Page $i'));
}),
),
);
}
// Start with viewport(1.0)
await tester.pumpWidget(build(scrollCacheExtent: const ScrollCacheExtent.viewport(1.0)));
expect(find.text('Page 1', skipOffstage: false), findsOneWidget);
// Update to viewport(2.0)
await tester.pumpWidget(build(scrollCacheExtent: const ScrollCacheExtent.viewport(2.0)));
expect(find.text('Page 1', skipOffstage: false), findsOneWidget);
expect(find.text('Page 2', skipOffstage: false), findsOneWidget);
// Update back to viewport(1.0)
await tester.pumpWidget(build(scrollCacheExtent: const ScrollCacheExtent.viewport(1.0)));
expect(find.text('Page 1', skipOffstage: false), findsOneWidget);
expect(find.text('Page 2', skipOffstage: false), findsNothing);
});
testWidgets('PageView semantics respects allowImplicitScrolling', (WidgetTester tester) async {
final semantics = SemanticsTester(tester);
// Off-screen pages should be excluded from semantics.
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: PageView(children: const <Widget>[Text('Page 1'), Text('Page 2')]),
),
);
expect(semantics, includesNodeWith(label: 'Page 1'));
expect(semantics, isNot(includesNodeWith(label: 'Page 2')));
// Off-screen pages should be included in semantics.
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: PageView(
allowImplicitScrolling: true,
children: const <Widget>[Text('Page 1'), Text('Page 2')],
),
),
);
expect(semantics, includesNodeWith(label: 'Page 1'));
expect(semantics, includesNodeWith(label: 'Page 2'));
semantics.dispose();
});
testWidgets(
'PageView asserts when scrollCacheExtent and allowImplicitScrolling are inconsistent',
(WidgetTester tester) async {
// allowImplicitScrolling: true && scrollCacheExtent: 0.0
expect(
() => PageView(
allowImplicitScrolling: true,
scrollCacheExtent: const ScrollCacheExtent.viewport(0.0),
children: const <Widget>[Text('Page 1'), Text('Page 2')],
),
throwsAssertionError,
);
// allowImplicitScrolling: false && scrollCacheExtent > 0
expect(
() => PageView(
scrollCacheExtent: const ScrollCacheExtent.viewport(2.0),
children: const <Widget>[Text('Page 1'), Text('Page 2')],
),
throwsAssertionError,
);
},
);
testWidgets('PageView showOnScreen scrolls when allowImplicitScrolling is true', (
WidgetTester tester,
) async {
final controller = PageController();
addTearDown(controller.dispose);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: PageView(
controller: controller,
scrollCacheExtent: const ScrollCacheExtent.viewport(1.0),
allowImplicitScrolling: true,
children: const <Widget>[Text('Page 1'), Text('Page 2'), Text('Page 3'), Text('Page 4')],
),
),
);
final Finder targetFinder = find.text('Page 2', skipOffstage: false);
expect(targetFinder, findsOneWidget);
final RenderObject target = tester.renderObject(targetFinder);
target.showOnScreen();
await tester.pumpAndSettle();
// Should scroll.
expect(controller.page, 1.0);
});
testWidgets(
'PageView showOnScreen scrolling behavior is consistent with default scrollCacheExtent',
(WidgetTester tester) async {
// This test verifies that providing an explicit scrollCacheExtent behaves consistently
// with PageView's default behavior when allowImplicitScrolling is true.
final controller = PageController();
addTearDown(controller.dispose);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: PageView(
controller: controller,
allowImplicitScrolling: true,
children: const <Widget>[
Text('Page 1'),
Text('Page 2'),
Text('Page 3'),
Text('Page 4'),
],
),
),
);
// Page 2 (index 1) is within the implicit cache extent.
final Finder targetFinder = find.text('Page 2', skipOffstage: false);
expect(targetFinder, findsOneWidget);
final RenderObject target = tester.renderObject(targetFinder);
target.showOnScreen();
await tester.pumpAndSettle();
// Should scroll (existing behavior).
expect(controller.page, 1.0);
},
);
testWidgets('PageView updates _ForceImplicitScrollPhysics when allowImplicitScrolling changes', (
WidgetTester tester,
) async {
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: PageView(children: const <Widget>[Text('Page 1'), Text('Page 2')]),
),
);
ScrollableState scrollable = tester.state(find.byType(Scrollable));
final ScrollPhysics initialPhysics = scrollable.position.physics;
expect(initialPhysics.allowImplicitScrolling, isFalse);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: PageView(
allowImplicitScrolling: true,
children: const <Widget>[Text('Page 1'), Text('Page 2')],
),
),
);
scrollable = tester.state(find.byType(Scrollable));
final ScrollPhysics updatedPhysics = scrollable.position.physics;
expect(updatedPhysics.allowImplicitScrolling, isTrue);
expect(identical(initialPhysics, updatedPhysics), isFalse);
});
testWidgets(
'PageView skips updating _ForceImplicitScrollPhysics when allowImplicitScrolling is unchanged',
(WidgetTester tester) async {
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: PageView(
allowImplicitScrolling: true,
children: const <Widget>[Text('Page 1'), Text('Page 2')],
),
),
);
final ScrollableState scrollable = tester.state(find.byType(Scrollable));
final ScrollPhysics initialPhysics = scrollable.position.physics;
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: PageView(
allowImplicitScrolling: true,
children: const <Widget>[Text('Page 1'), Text('Page 2')],
),
),
);
final ScrollPhysics currentPhysics = scrollable.position.physics;
expect(identical(initialPhysics, currentPhysics), isTrue);
},
);
}