// 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(['reduced-test-set']) library; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart' show DragStartBehavior; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; import '../rendering/rendering_tester.dart' show TestClipPaintingContext; import 'semantics_tester.dart'; import 'states.dart'; void main() { // 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((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 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((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); // Change the page through the page controller when zero viewport controller.animateToPage( kStates.indexOf('Iowa'), duration: kTabScrollDuration, 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((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 = []; await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: PageView( dragStartBehavior: DragStartBehavior.down, children: kStates.map((String state) { return GestureDetector( dragStartBehavior: DragStartBehavior.down, onTap: () { log.add(state); }, child: Container(height: 200.0, color: const Color(0xFF0000FF), child: Text(state)), ); }).toList(), ), ), ); await tester.tap(find.text('Alabama')); expect(log, equals(['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(['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( MaterialApp( home: PageView( children: List.generate(10, (int i) { return Container(key: ValueKey(i), color: const Color(0xFF0000FF)); }), ), ), ); Size sizeOf(int i) => tester.getSize(find.byKey(ValueKey(i))); double leftOf(int i) => tester.getTopLeft(find.byKey(ValueKey(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.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((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((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((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((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((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((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((String state) => Text(state)).toList()), ), ), ), ); expect(find.text('Alabama', skipOffstage: false), findsOneWidget); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Center( child: SizedBox( width: 200.0, height: 200.0, child: PageView(children: kStates.map((String state) => Text(state)).toList()), ), ), ), ); expect(find.text('Alabama'), findsOneWidget); }); testWidgets('Page changes at halfway point', (WidgetTester tester) async { final log = []; await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: PageView( onPageChanged: log.add, children: kStates.map((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 [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 = []; 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((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 ? const Color(0xFF0000FF) : const Color(0xFF00FF00), 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 = []; Widget build({required bool pageSnapping}) { return Directionality( textDirection: TextDirection.ltr, child: PageView( pageSnapping: pageSnapping, onPageChanged: log.add, children: kStates.map((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 [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 [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 ? const Color(0xFF0000FF) : const Color(0xFF00FF00), 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 ? const Color(0xFF0000FF) : const Color(0xFF00FF00), 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 ? const Color(0xFF0000FF) : const Color(0xFF00FF00), 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 ? const Color(0xFF0000FF) : const Color(0xFF00FF00), 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 visiblePages = const [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 [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((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((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('PageView'), controller: controller, children: const [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('PageView'), controller: controller, children: const [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( 'Check it again against your list and see consistency!', ), controller: controller2, children: const [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.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.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.generate(4, (int i) { return Semantics(container: true, child: Text('Page #$i')); }), ), ), ); expect(controller.page, 0); expect(semantics, includesNodeWith(flags: [SemanticsFlag.hasImplicitScrolling])); expect(semantics, includesNodeWith(label: 'Page #0')); expect( semantics, includesNodeWith(label: 'Page #1', flags: [SemanticsFlag.isHidden]), ); expect( semantics, isNot(includesNodeWith(label: 'Page #2', flags: [SemanticsFlag.isHidden])), ); expect( semantics, isNot(includesNodeWith(label: 'Page #3', flags: [SemanticsFlag.isHidden])), ); controller.nextPage(duration: const Duration(milliseconds: 150), curve: Curves.ease); await tester.pumpAndSettle(); expect( semantics, includesNodeWith(label: 'Page #0', flags: [SemanticsFlag.isHidden]), ); expect(semantics, includesNodeWith(label: 'Page #1')); expect( semantics, includesNodeWith(label: 'Page #2', flags: [SemanticsFlag.isHidden]), ); expect( semantics, isNot(includesNodeWith(label: 'Page #3', flags: [SemanticsFlag.isHidden])), ); controller.nextPage(duration: const Duration(milliseconds: 150), curve: Curves.ease); await tester.pumpAndSettle(); expect( semantics, isNot(includesNodeWith(label: 'Page #0', flags: [SemanticsFlag.isHidden])), ); expect( semantics, includesNodeWith(label: 'Page #1', flags: [SemanticsFlag.isHidden]), ); expect(semantics, includesNodeWith(label: 'Page #2')); expect( semantics, includesNodeWith(label: 'Page #3', flags: [SemanticsFlag.isHidden]), ); semantics.dispose(); }); testWidgets('PageView respects clipBehavior', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: PageView(children: [Container(height: 2000.0)]), ), ); // 1st, check that the render object has received the default clip behavior. final RenderViewport renderObject = tester.allRenderObjects.whereType().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: [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(viewportFinder()).padEnds, true); // PageView(padEnds: false) is propagated properly. await tester.pumpWidget( Directionality(textDirection: TextDirection.ltr, child: PageView(padEnds: false)), ); expect(tester.widget(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; await tester.pumpWidget( MaterialApp( home: Center( child: SizedBox( width: pixel6EmulatorWidth, child: PageView( controller: controller, physics: const PageScrollPhysics().applyTo(const ClampingScrollPhysics()), children: const [ 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( MaterialApp( home: Center( child: PageView( controller: controller, physics: const PageScrollPhysics().applyTo(const ClampingScrollPhysics()), children: const [ 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 MaterialApp( home: Scaffold( body: PageView( key: key, controller: controller, children: const [ Center(child: Text('0')), Center(child: Text('1')), Center(child: Text('2')), ], ), ), ); } Future testPageViewWithController( PageController controller, WidgetTester tester, bool controls, ) async { int currentVisiblePage() { return int.parse(tester.widgetList(find.byType(Text)).whereType().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 MaterialApp( home: Scaffold( body: PageView( controller: controller, children: [ Container(color: Colors.red), Container(color: Colors.green), Container(color: Colors.blue), ], ), ), ); } group('One pageController is attached to multiple PageViews', () { Widget createMultiplePageViews(PageController controller) { return MaterialApp( home: Scaffold( body: Column( children: [ Expanded( child: PageView( controller: controller, children: [ Container(color: Colors.red), Container(color: Colors.green), Container(color: Colors.blue), ], ), ), Expanded( child: PageView( controller: controller, children: [ Container(color: Colors.orange), Container(color: Colors.purple), Container(color: Colors.yellow), ], ), ), ], ), ), ); } 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( MaterialApp( home: Material( child: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return Scaffold( body: PageView( controller: controller, children: [ Builder( builder: (BuildContext context) { currentPage = controller.page == null ? 'null' : 'not empty'; return Center(child: Text(currentPage)); }, ), ], ), floatingActionButton: FloatingActionButton( onPressed: () { setState(() {}); }, ), ); }, ), ), ), ); expect(find.text('null'), findsOneWidget); expect(find.text('not empty'), findsNothing); expect(currentPage, 'null'); await tester.tap(find.byType(FloatingActionButton)); 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( MaterialApp( home: Scaffold( body: Navigator( onDidRemovePage: (Page page) {}, pages: >[ MaterialPage( child: Scaffold( body: PageView( controller: controller, children: const [ Scaffold(body: Text('One')), Scaffold(body: Text('Two')), ], ), ), ), const MaterialPage(child: Scaffold()), ], ), ), ), ); 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( MaterialApp( home: Scaffold( body: Navigator( onDidRemovePage: (Page page) {}, pages: >[ MaterialPage( child: Scaffold( body: PageView( controller: controller, children: const [ Scaffold(body: Text('One')), Scaffold(body: Text('Two')), ], ), ), ), const MaterialPage(child: Scaffold()), ], ), ), ), ); controller.animateToPage(1, duration: const Duration(milliseconds: 50), curve: Curves.bounceIn); expect(tester.takeException(), null); }); }