// Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'package:flutter/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'; import 'semantics_tester.dart'; import 'states.dart'; const Duration _frameDuration = Duration(milliseconds: 100); void main() { testWidgets('PageView control test', (WidgetTester tester) async { final List<String> 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: const Color(0xFF0000FF), 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(_frameDuration); 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(_frameDuration); 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(_frameDuration); 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(_frameDuration); 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<Widget>.generate(10, (int i) { return Container( key: ValueKey<int>(i), color: const Color(0xFF0000FF), ); }), ), )); 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. 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 PageController controller = PageController(initialPage: 4); 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(const Duration(milliseconds: 100)); 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(const Duration(milliseconds: 100)); 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(const Duration(milliseconds: 100)); 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 PageController controller = PageController(); await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, child: PageView( controller: controller, children: kStates.map<Widget>((String state) => Text(state)).toList(), ), )); bool 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); bool 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( width: 0.0, height: 0.0, 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( width: 200.0, height: 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 List<int> 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 List<int> log = <int>[]; final PageController controller = PageController(viewportFraction: 0.9); 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 { PageController controller = PageController(viewportFraction: 7/8); 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); 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 List<int> 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 PageController controller = PageController(viewportFraction: 1/8); 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 PageController controller = PageController(viewportFraction: 5/4); 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 PageController oldController = PageController(viewportFraction: 5/4); 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 PageController newController = PageController(viewportFraction: 4); 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 PageController controller = PageController(viewportFraction: 5/4); 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 PageController controller = PageController(viewportFraction: 1/4, initialPage: 0); 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 int 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 int 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 PageController controller = PageController( initialPage: kStates.length - 1, viewportFraction: 0.5, ); 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 PageController controller = PageController( initialPage: kStates.length - 1, ); int 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 PageController controller = PageController(); 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 PageStorageBucket 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)), 1); 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 PageController controller2 = PageController(keepPage: false); 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 SemanticsTester semantics = SemanticsTester(tester); final PageController controller = PageController(); 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 PageMetrics page = PageMetrics( minScrollExtent: 100.0, maxScrollExtent: 200.0, pixels: 150.0, viewportDimension: 25.0, axisDirection: AxisDirection.right, viewportFraction: 1.0, ); 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 = PageController(); 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 SemanticsTester semantics = SemanticsTester(tester); final PageController controller = PageController(); 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 TestClipPaintingContext 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( children: const <Widget>[], ), )); expect(tester.widget<SliverFillViewport>(viewportFinder()).padEnds, true); // PageView(padEnds: false) is propagated properly. await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, child: PageView( padEnds: false, children: const <Widget>[], ), )); expect(tester.widget<SliverFillViewport>(viewportFinder()).padEnds, false); }); }