// 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 'dart:math' as math; import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'two_dimensional_utils.dart'; Finder findKey(int i) => find.byKey(ValueKey<int>(i), skipOffstage: false); Widget buildSingleChildScrollView(Axis scrollDirection, { bool reverse = false }) { return Directionality( textDirection: TextDirection.ltr, child: Center( child: SizedBox( width: 600.0, height: 400.0, child: SingleChildScrollView( scrollDirection: scrollDirection, reverse: reverse, child: ListBody( mainAxis: scrollDirection, children: const <Widget>[ SizedBox(key: ValueKey<int>(0), width: 200.0, height: 200.0), SizedBox(key: ValueKey<int>(1), width: 200.0, height: 200.0), SizedBox(key: ValueKey<int>(2), width: 200.0, height: 200.0), SizedBox(key: ValueKey<int>(3), width: 200.0, height: 200.0), SizedBox(key: ValueKey<int>(4), width: 200.0, height: 200.0), SizedBox(key: ValueKey<int>(5), width: 200.0, height: 200.0), SizedBox(key: ValueKey<int>(6), width: 200.0, height: 200.0), ], ), ), ), ), ); } Widget buildListView(Axis scrollDirection, { bool reverse = false, bool shrinkWrap = false }) { return Directionality( textDirection: TextDirection.ltr, child: Center( child: SizedBox( width: 600.0, height: 400.0, child: ListView( scrollDirection: scrollDirection, reverse: reverse, addSemanticIndexes: false, shrinkWrap: shrinkWrap, children: const <Widget>[ SizedBox(key: ValueKey<int>(0), width: 200.0, height: 200.0), SizedBox(key: ValueKey<int>(1), width: 200.0, height: 200.0), SizedBox(key: ValueKey<int>(2), width: 200.0, height: 200.0), SizedBox(key: ValueKey<int>(3), width: 200.0, height: 200.0), SizedBox(key: ValueKey<int>(4), width: 200.0, height: 200.0), SizedBox(key: ValueKey<int>(5), width: 200.0, height: 200.0), SizedBox(key: ValueKey<int>(6), width: 200.0, height: 200.0), ], ), ), ), ); } void main() { group('SingleChildScrollView', () { testWidgets('SingleChildScrollView ensureVisible Axis.vertical', (WidgetTester tester) async { BuildContext findContext(int i) => tester.element(findKey(i)); await tester.pumpWidget(buildSingleChildScrollView(Axis.vertical)); Scrollable.ensureVisible(findContext(3)); await tester.pump(); expect(tester.getTopLeft(findKey(3)).dy, equals(100.0)); Scrollable.ensureVisible(findContext(6)); await tester.pump(); expect(tester.getTopLeft(findKey(6)).dy, equals(300.0)); Scrollable.ensureVisible(findContext(4), alignment: 1.0); await tester.pump(); expect(tester.getBottomRight(findKey(4)).dy, equals(500.0)); Scrollable.ensureVisible(findContext(0), alignment: 1.0); await tester.pump(); expect(tester.getTopLeft(findKey(0)).dy, equals(100.0)); Scrollable.ensureVisible(findContext(3), duration: const Duration(seconds: 1)); await tester.pump(); await tester.pump(const Duration(milliseconds: 1020)); expect(tester.getTopLeft(findKey(3)).dy, equals(100.0)); }); testWidgets('SingleChildScrollView ensureVisible Axis.horizontal', (WidgetTester tester) async { BuildContext findContext(int i) => tester.element(findKey(i)); await tester.pumpWidget(buildSingleChildScrollView(Axis.horizontal)); Scrollable.ensureVisible(findContext(3)); await tester.pump(); expect(tester.getTopLeft(findKey(3)).dx, equals(100.0)); Scrollable.ensureVisible(findContext(6)); await tester.pump(); expect(tester.getTopLeft(findKey(6)).dx, equals(500.0)); Scrollable.ensureVisible(findContext(4), alignment: 1.0); await tester.pump(); expect(tester.getBottomRight(findKey(4)).dx, equals(700.0)); Scrollable.ensureVisible(findContext(0), alignment: 1.0); await tester.pump(); expect(tester.getTopLeft(findKey(0)).dx, equals(100.0)); Scrollable.ensureVisible(findContext(3), duration: const Duration(seconds: 1)); await tester.pump(); await tester.pump(const Duration(milliseconds: 1020)); expect(tester.getTopLeft(findKey(3)).dx, equals(100.0)); }); testWidgets('SingleChildScrollView ensureVisible Axis.vertical reverse', (WidgetTester tester) async { BuildContext findContext(int i) => tester.element(findKey(i)); await tester.pumpWidget(buildSingleChildScrollView(Axis.vertical, reverse: true)); Scrollable.ensureVisible(findContext(3)); await tester.pump(); expect(tester.getBottomRight(findKey(3)).dy, equals(500.0)); Scrollable.ensureVisible(findContext(0)); await tester.pump(); expect(tester.getBottomRight(findKey(0)).dy, equals(300.0)); Scrollable.ensureVisible(findContext(2), alignment: 1.0); await tester.pump(); expect(tester.getTopLeft(findKey(2)).dy, equals(100.0)); Scrollable.ensureVisible(findContext(6), alignment: 1.0); await tester.pump(); expect(tester.getBottomRight(findKey(6)).dy, equals(500.0)); Scrollable.ensureVisible(findContext(3), duration: const Duration(seconds: 1)); await tester.pump(); await tester.pump(const Duration(milliseconds: 1020)); expect(tester.getBottomRight(findKey(3)).dy, equals(500.0)); // Regression test for https://github.com/flutter/flutter/issues/128749 // Reset to zero position. tester.state<ScrollableState>(find.byType(Scrollable)).position.jumpTo(0.0); await tester.pump(); // 4 is not currently visible as the SingleChildScrollView is contained // within a centered SizedBox. expect(tester.getBottomLeft(findKey(4)).dy, equals(100.0)); expect(tester.getBottomLeft(findKey(6)).dy, equals(500.0)); Scrollable.ensureVisible( findContext(6), alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtStart, ); await tester.pump(); Scrollable.ensureVisible( findContext(5), alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtStart, ); await tester.pump(); // 5 and 6 are already visible beyond the top edge, so no change. expect(tester.getBottomLeft(findKey(4)).dy, equals(100.0)); expect(tester.getBottomLeft(findKey(6)).dy, equals(500.0)); Scrollable.ensureVisible( findContext(4), alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtStart, ); await tester.pump(); // Since it is reversed, 4 should have come into view at the top // edge of the scrollable, matching the alignment expectation. expect(tester.getBottomLeft(findKey(4)).dy, equals(300.0)); expect(tester.getBottomLeft(findKey(6)).dy, equals(700.0)); // Bring 6 back into view at the trailing edge, checking the other // alignment. Scrollable.ensureVisible( findContext(6), alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtEnd, ); await tester.pump(); expect(tester.getBottomLeft(findKey(4)).dy, equals(100.0)); expect(tester.getBottomLeft(findKey(6)).dy, equals(500.0)); }); testWidgets('SingleChildScrollView ensureVisible Axis.horizontal reverse', (WidgetTester tester) async { BuildContext findContext(int i) => tester.element(findKey(i)); await tester.pumpWidget(buildSingleChildScrollView(Axis.horizontal, reverse: true)); Scrollable.ensureVisible(findContext(3)); await tester.pump(); expect(tester.getBottomRight(findKey(3)).dx, equals(700.0)); Scrollable.ensureVisible(findContext(0)); await tester.pump(); expect(tester.getBottomRight(findKey(0)).dx, equals(300.0)); Scrollable.ensureVisible(findContext(2), alignment: 1.0); await tester.pump(); expect(tester.getTopLeft(findKey(2)).dx, equals(100.0)); Scrollable.ensureVisible(findContext(6), alignment: 1.0); await tester.pump(); expect(tester.getBottomRight(findKey(6)).dx, equals(700.0)); Scrollable.ensureVisible(findContext(3), duration: const Duration(seconds: 1)); await tester.pump(); await tester.pump(const Duration(milliseconds: 1020)); expect(tester.getBottomRight(findKey(3)).dx, equals(700.0)); // Regression test for https://github.com/flutter/flutter/issues/128749 // Reset to zero position. tester.state<ScrollableState>(find.byType(Scrollable)).position.jumpTo(0.0); await tester.pump(); // 4 is not currently visible as the SingleChildScrollView is contained // within a centered SizedBox. expect(tester.getBottomLeft(findKey(3)).dx, equals(-100.0)); expect(tester.getBottomLeft(findKey(6)).dx, equals(500.0)); Scrollable.ensureVisible( findContext(6), alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtStart, ); await tester.pump(); Scrollable.ensureVisible( findContext(5), alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtStart, ); await tester.pump(); Scrollable.ensureVisible( findContext(4), alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtStart, ); await tester.pump(); // 4, 5 and 6 are already visible beyond the left edge, so no change. expect(tester.getBottomLeft(findKey(3)).dx, equals(-100.0)); expect(tester.getBottomLeft(findKey(6)).dx, equals(500.0)); Scrollable.ensureVisible( findContext(3), alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtStart, ); await tester.pump(); // Since it is reversed, 3 should have come into view at the leading // edge of the scrollable, matching the alignment expectation. expect(tester.getBottomLeft(findKey(3)).dx, equals(100.0)); expect(tester.getBottomLeft(findKey(6)).dx, equals(700.0)); // Bring 6 back into view at the trailing edge, checking the other // alignment. Scrollable.ensureVisible( findContext(6), alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtEnd, ); await tester.pump(); expect(tester.getBottomLeft(findKey(3)).dx, equals(-100.0)); expect(tester.getBottomLeft(findKey(6)).dx, equals(500.0)); }); testWidgets('SingleChildScrollView ensureVisible rotated child', (WidgetTester tester) async { BuildContext findContext(int i) => tester.element(findKey(i)); await tester.pumpWidget( Center( child: SizedBox( width: 600.0, height: 400.0, child: SingleChildScrollView( child: ListBody( children: <Widget>[ const SizedBox(height: 200.0), const SizedBox(height: 200.0), const SizedBox(height: 200.0), SizedBox( height: 200.0, child: Center( child: Transform( transform: Matrix4.rotationZ(math.pi), child: Container( key: const ValueKey<int>(0), width: 100.0, height: 100.0, color: const Color(0xFFFFFFFF), ), ), ), ), const SizedBox(height: 200.0), const SizedBox(height: 200.0), const SizedBox(height: 200.0), ], ), ), ), ), ); Scrollable.ensureVisible(findContext(0)); await tester.pump(); expect(tester.getBottomRight(findKey(0)).dy, moreOrLessEquals(100.0, epsilon: 0.1)); Scrollable.ensureVisible(findContext(0), alignment: 1.0); await tester.pump(); expect(tester.getTopLeft(findKey(0)).dy, moreOrLessEquals(500.0, epsilon: 0.1)); }); testWidgets('Nested SingleChildScrollView ensureVisible behavior test', (WidgetTester tester) async { // Regressing test for https://github.com/flutter/flutter/issues/65100 Finder findKey(String coordinate) => find.byKey(ValueKey<String>(coordinate)); BuildContext findContext(String coordinate) => tester.element(findKey(coordinate)); final List<Row> rows = List<Row>.generate( 7, (int y) => Row( children: List<SizedBox>.generate( 7, (int x) => SizedBox(key: ValueKey<String>('$x, $y'), width: 200.0, height: 200.0), ), ), ); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Center( child: SizedBox( width: 600.0, height: 400.0, child: SingleChildScrollView( scrollDirection: Axis.horizontal, child: SingleChildScrollView( child: Column( children: rows, ), ), ), ), ), ), ); // Items: 7 * 7 Container(width: 200.0, height: 200.0) // viewport: Size(width: 600.0, height: 400.0) // // 0 600 // +----------------------+ // |0,0 |1,0 |2,0 | // | | | | // +----------------------+ // |0,1 |1,1 |2,1 | // | | | | // 400 +----------------------+ Scrollable.ensureVisible(findContext('0, 0')); await tester.pump(); expect(tester.getTopLeft(findKey('0, 0')), const Offset(100.0, 100.0)); Scrollable.ensureVisible(findContext('3, 0')); await tester.pump(); expect(tester.getTopLeft(findKey('3, 0')), const Offset(100.0, 100.0)); Scrollable.ensureVisible(findContext('3, 0'), alignment: 0.5); await tester.pump(); expect(tester.getTopLeft(findKey('3, 0')), const Offset(300.0, 100.0)); Scrollable.ensureVisible(findContext('6, 0')); await tester.pump(); expect(tester.getTopLeft(findKey('6, 0')), const Offset(500.0, 100.0)); Scrollable.ensureVisible(findContext('0, 2')); await tester.pump(); expect(tester.getTopLeft(findKey('0, 2')), const Offset(100.0, 100.0)); Scrollable.ensureVisible(findContext('3, 2')); await tester.pump(); expect(tester.getTopLeft(findKey('3, 2')), const Offset(100.0, 100.0)); // It should be at the center of the screen. Scrollable.ensureVisible(findContext('3, 2'), alignment: 0.5); await tester.pump(); expect(tester.getTopLeft(findKey('3, 2')), const Offset(300.0, 200.0)); }); }); group('ListView', () { testWidgets('ListView ensureVisible Axis.vertical', (WidgetTester tester) async { BuildContext findContext(int i) => tester.element(findKey(i)); Future<void> prepare(double offset) async { tester.state<ScrollableState>(find.byType(Scrollable)).position.jumpTo(offset); await tester.pump(); } await tester.pumpWidget(buildListView(Axis.vertical)); await prepare(480.0); Scrollable.ensureVisible(findContext(3)); await tester.pump(); expect(tester.getTopLeft(findKey(3)).dy, equals(100.0)); await prepare(1083.0); Scrollable.ensureVisible(findContext(6)); await tester.pump(); expect(tester.getTopLeft(findKey(6)).dy, equals(300.0)); await prepare(735.0); Scrollable.ensureVisible(findContext(4), alignment: 1.0); await tester.pump(); expect(tester.getBottomRight(findKey(4)).dy, equals(500.0)); await prepare(123.0); Scrollable.ensureVisible(findContext(0), alignment: 1.0); await tester.pump(); expect(tester.getTopLeft(findKey(0)).dy, equals(100.0)); await prepare(523.0); Scrollable.ensureVisible(findContext(3), duration: const Duration(seconds: 1)); await tester.pump(); await tester.pump(const Duration(milliseconds: 1020)); expect(tester.getTopLeft(findKey(3)).dy, equals(100.0)); }); testWidgets('ListView ensureVisible Axis.horizontal', (WidgetTester tester) async { BuildContext findContext(int i) => tester.element(findKey(i)); Future<void> prepare(double offset) async { tester.state<ScrollableState>(find.byType(Scrollable)).position.jumpTo(offset); await tester.pump(); } await tester.pumpWidget(buildListView(Axis.horizontal)); await prepare(23.0); Scrollable.ensureVisible(findContext(3)); await tester.pump(); expect(tester.getTopLeft(findKey(3)).dx, equals(100.0)); await prepare(843.0); Scrollable.ensureVisible(findContext(6)); await tester.pump(); expect(tester.getTopLeft(findKey(6)).dx, equals(500.0)); await prepare(415.0); Scrollable.ensureVisible(findContext(4), alignment: 1.0); await tester.pump(); expect(tester.getBottomRight(findKey(4)).dx, equals(700.0)); await prepare(46.0); Scrollable.ensureVisible(findContext(0), alignment: 1.0); await tester.pump(); expect(tester.getTopLeft(findKey(0)).dx, equals(100.0)); await prepare(211.0); Scrollable.ensureVisible(findContext(3), duration: const Duration(seconds: 1)); await tester.pump(); await tester.pump(const Duration(milliseconds: 1020)); expect(tester.getTopLeft(findKey(3)).dx, equals(100.0)); }); testWidgets('ListView ensureVisible Axis.vertical reverse', (WidgetTester tester) async { BuildContext findContext(int i) => tester.element(findKey(i)); Future<void> prepare(double offset) async { tester.state<ScrollableState>(find.byType(Scrollable)).position.jumpTo(offset); await tester.pump(); } await tester.pumpWidget(buildListView(Axis.vertical, reverse: true)); await prepare(211.0); Scrollable.ensureVisible(findContext(3)); await tester.pump(); expect(tester.getBottomRight(findKey(3)).dy, equals(500.0)); await prepare(23.0); Scrollable.ensureVisible(findContext(0)); await tester.pump(); expect(tester.getBottomRight(findKey(0)).dy, equals(500.0)); await prepare(230.0); Scrollable.ensureVisible(findContext(2), alignment: 1.0); await tester.pump(); expect(tester.getTopLeft(findKey(2)).dy, equals(100.0)); await prepare(1083.0); Scrollable.ensureVisible(findContext(6), alignment: 1.0); await tester.pump(); expect(tester.getBottomRight(findKey(6)).dy, equals(300.0)); await prepare(345.0); Scrollable.ensureVisible(findContext(3), duration: const Duration(seconds: 1)); await tester.pump(); await tester.pump(const Duration(milliseconds: 1020)); expect(tester.getBottomRight(findKey(3)).dy, equals(500.0)); // Regression test for https://github.com/flutter/flutter/issues/128749 // Reset to zero position. await prepare(0.0); // 2 is not currently visible as the ListView is contained // within a centered SizedBox. expect(tester.getBottomLeft(findKey(2)).dy, equals(100.0)); expect(tester.getBottomLeft(findKey(0)).dy, equals(500.0)); Scrollable.ensureVisible( findContext(0), alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtStart, ); await tester.pump(); Scrollable.ensureVisible( findContext(1), alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtStart, ); await tester.pump(); // 0 and 1 are already visible beyond the top edge, so no change. expect(tester.getBottomLeft(findKey(2)).dy, equals(100.0)); expect(tester.getBottomLeft(findKey(0)).dy, equals(500.0)); Scrollable.ensureVisible( findContext(2), alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtStart, ); await tester.pump(); // Since it is reversed, 2 should have come into view at the top // edge of the scrollable, matching the alignment expectation. expect(tester.getBottomLeft(findKey(2)).dy, equals(300.0)); expect(tester.getBottomLeft(findKey(0)).dy, equals(700.0)); // Bring 0 back into view at the trailing edge, checking the other // alignment. Scrollable.ensureVisible( findContext(0), alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtEnd, ); await tester.pump(); expect(tester.getBottomLeft(findKey(2)).dy, equals(100.0)); expect(tester.getBottomLeft(findKey(0)).dy, equals(500.0)); }); testWidgets('ListView ensureVisible Axis.horizontal reverse', (WidgetTester tester) async { BuildContext findContext(int i) => tester.element(findKey(i)); Future<void> prepare(double offset) async { tester.state<ScrollableState>(find.byType(Scrollable)).position.jumpTo(offset); await tester.pump(); } await tester.pumpWidget(buildListView(Axis.horizontal, reverse: true)); await prepare(211.0); Scrollable.ensureVisible(findContext(3)); await tester.pump(); expect(tester.getBottomRight(findKey(3)).dx, equals(700.0)); await prepare(23.0); Scrollable.ensureVisible(findContext(0)); await tester.pump(); expect(tester.getBottomRight(findKey(0)).dx, equals(700.0)); await prepare(230.0); Scrollable.ensureVisible(findContext(2), alignment: 1.0); await tester.pump(); expect(tester.getTopLeft(findKey(2)).dx, equals(100.0)); await prepare(1083.0); Scrollable.ensureVisible(findContext(6), alignment: 1.0); await tester.pump(); expect(tester.getBottomRight(findKey(6)).dx, equals(300.0)); await prepare(345.0); Scrollable.ensureVisible(findContext(3), duration: const Duration(seconds: 1)); await tester.pump(); await tester.pump(const Duration(milliseconds: 1020)); expect(tester.getBottomRight(findKey(3)).dx, equals(700.0)); // Regression test for https://github.com/flutter/flutter/issues/128749 // Reset to zero position. await prepare(0.0); // 3 is not currently visible as the ListView is contained // within a centered SizedBox. expect(tester.getBottomLeft(findKey(3)).dx, equals(-100.0)); expect(tester.getBottomLeft(findKey(0)).dx, equals(500.0)); Scrollable.ensureVisible( findContext(0), alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtStart, ); await tester.pump(); Scrollable.ensureVisible( findContext(1), alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtStart, ); await tester.pump(); Scrollable.ensureVisible( findContext(2), alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtStart, ); await tester.pump(); // 0, 1 and 2 are already visible beyond the left edge, so no change. expect(tester.getBottomLeft(findKey(3)).dx, equals(-100.0)); expect(tester.getBottomLeft(findKey(0)).dx, equals(500.0)); Scrollable.ensureVisible( findContext(3), alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtStart, ); await tester.pump(); // Since it is reversed, 3 should have come into view at the leading // edge of the scrollable, matching the alignment expectation. expect(tester.getBottomLeft(findKey(3)).dx, equals(100.0)); expect(tester.getBottomLeft(findKey(0)).dx, equals(700.0)); // Bring 0 back into view at the trailing edge, checking the other // alignment. Scrollable.ensureVisible( findContext(0), alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtEnd, ); await tester.pump(); expect(tester.getBottomLeft(findKey(3)).dx, equals(-100.0)); expect(tester.getBottomLeft(findKey(0)).dx, equals(500.0)); }); testWidgets('ListView ensureVisible negative child', (WidgetTester tester) async { BuildContext findContext(int i) => tester.element(findKey(i)); Future<void> prepare(double offset) async { tester.state<ScrollableState>(find.byType(Scrollable)).position.jumpTo(offset); await tester.pump(); } double getOffset() { return tester.state<ScrollableState>(find.byType(Scrollable)).position.pixels; } Widget buildSliver(int i) { return SliverToBoxAdapter( key: ValueKey<int>(i), child: const SizedBox(width: 200.0, height: 200.0), ); } await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Center( child: SizedBox( width: 600.0, height: 400.0, child: Scrollable( viewportBuilder: (BuildContext context, ViewportOffset offset) { return Viewport( offset: offset, center: const ValueKey<int>(4), slivers: <Widget>[ buildSliver(0), buildSliver(1), buildSliver(2), buildSliver(3), buildSliver(4), buildSliver(5), buildSliver(6), ], ); }, ), ), ), ), ); await prepare(-125.0); Scrollable.ensureVisible(findContext(3)); await tester.pump(); expect(getOffset(), equals(-200.0)); await prepare(-225.0); Scrollable.ensureVisible(findContext(2)); await tester.pump(); expect(getOffset(), equals(-400.0)); }); testWidgets('ListView ensureVisible rotated child', (WidgetTester tester) async { BuildContext findContext(int i) => tester.element(findKey(i)); Future<void> prepare(double offset) async { tester.state<ScrollableState>(find.byType(Scrollable)).position.jumpTo(offset); await tester.pump(); } await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, child: Center( child: SizedBox( width: 600.0, height: 400.0, child: ListView( children: <Widget>[ const SizedBox(height: 200.0), const SizedBox(height: 200.0), const SizedBox(height: 200.0), SizedBox( height: 200.0, child: Center( child: Transform( transform: Matrix4.rotationZ(math.pi), child: Container( key: const ValueKey<int>(0), width: 100.0, height: 100.0, color: const Color(0xFFFFFFFF), ), ), ), ), const SizedBox(height: 200.0), const SizedBox(height: 200.0), const SizedBox(height: 200.0), ], ), ), ), )); await prepare(321.0); Scrollable.ensureVisible(findContext(0)); await tester.pump(); expect(tester.getBottomRight(findKey(0)).dy, moreOrLessEquals(100.0, epsilon: 0.1)); Scrollable.ensureVisible(findContext(0), alignment: 1.0); await tester.pump(); expect(tester.getTopLeft(findKey(0)).dy, moreOrLessEquals(500.0, epsilon: 0.1)); }); }); group('ListView shrinkWrap', () { testWidgets('ListView ensureVisible Axis.vertical', (WidgetTester tester) async { BuildContext findContext(int i) => tester.element(findKey(i)); Future<void> prepare(double offset) async { tester.state<ScrollableState>(find.byType(Scrollable)).position.jumpTo(offset); await tester.pump(); } await tester.pumpWidget(buildListView(Axis.vertical, shrinkWrap: true)); await prepare(480.0); Scrollable.ensureVisible(findContext(3)); await tester.pump(); expect(tester.getTopLeft(findKey(3)).dy, equals(100.0)); await prepare(1083.0); Scrollable.ensureVisible(findContext(6)); await tester.pump(); expect(tester.getTopLeft(findKey(6)).dy, equals(300.0)); await prepare(735.0); Scrollable.ensureVisible(findContext(4), alignment: 1.0); await tester.pump(); expect(tester.getBottomRight(findKey(4)).dy, equals(500.0)); await prepare(123.0); Scrollable.ensureVisible(findContext(0), alignment: 1.0); await tester.pump(); expect(tester.getTopLeft(findKey(0)).dy, equals(100.0)); await prepare(523.0); Scrollable.ensureVisible(findContext(3), duration: const Duration(seconds: 1)); await tester.pump(); await tester.pump(const Duration(milliseconds: 1020)); expect(tester.getTopLeft(findKey(3)).dy, equals(100.0)); }); testWidgets('ListView ensureVisible Axis.horizontal', (WidgetTester tester) async { BuildContext findContext(int i) => tester.element(findKey(i)); Future<void> prepare(double offset) async { tester.state<ScrollableState>(find.byType(Scrollable)).position.jumpTo(offset); await tester.pump(); } await tester.pumpWidget(buildListView(Axis.horizontal, shrinkWrap: true)); await prepare(23.0); Scrollable.ensureVisible(findContext(3)); await tester.pump(); expect(tester.getTopLeft(findKey(3)).dx, equals(100.0)); await prepare(843.0); Scrollable.ensureVisible(findContext(6)); await tester.pump(); expect(tester.getTopLeft(findKey(6)).dx, equals(500.0)); await prepare(415.0); Scrollable.ensureVisible(findContext(4), alignment: 1.0); await tester.pump(); expect(tester.getBottomRight(findKey(4)).dx, equals(700.0)); await prepare(46.0); Scrollable.ensureVisible(findContext(0), alignment: 1.0); await tester.pump(); expect(tester.getTopLeft(findKey(0)).dx, equals(100.0)); await prepare(211.0); Scrollable.ensureVisible(findContext(3), duration: const Duration(seconds: 1)); await tester.pump(); await tester.pump(const Duration(milliseconds: 1020)); expect(tester.getTopLeft(findKey(3)).dx, equals(100.0)); }); testWidgets('ListView ensureVisible Axis.vertical reverse', (WidgetTester tester) async { BuildContext findContext(int i) => tester.element(findKey(i)); Future<void> prepare(double offset) async { tester.state<ScrollableState>(find.byType(Scrollable)).position.jumpTo(offset); await tester.pump(); } await tester.pumpWidget(buildListView(Axis.vertical, reverse: true, shrinkWrap: true)); await prepare(211.0); Scrollable.ensureVisible(findContext(3)); await tester.pump(); expect(tester.getBottomRight(findKey(3)).dy, equals(500.0)); await prepare(23.0); Scrollable.ensureVisible(findContext(0)); await tester.pump(); expect(tester.getBottomRight(findKey(0)).dy, equals(500.0)); await prepare(230.0); Scrollable.ensureVisible(findContext(2), alignment: 1.0); await tester.pump(); expect(tester.getTopLeft(findKey(2)).dy, equals(100.0)); await prepare(1083.0); Scrollable.ensureVisible(findContext(6), alignment: 1.0); await tester.pump(); expect(tester.getBottomRight(findKey(6)).dy, equals(300.0)); await prepare(345.0); Scrollable.ensureVisible(findContext(3), duration: const Duration(seconds: 1)); await tester.pump(); await tester.pump(const Duration(milliseconds: 1020)); expect(tester.getBottomRight(findKey(3)).dy, equals(500.0)); // Regression test for https://github.com/flutter/flutter/issues/128749 // Reset to zero position. await prepare(0.0); // 2 is not currently visible as the ListView is contained // within a centered SizedBox. expect(tester.getBottomLeft(findKey(2)).dy, equals(100.0)); expect(tester.getBottomLeft(findKey(0)).dy, equals(500.0)); Scrollable.ensureVisible( findContext(0), alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtStart, ); await tester.pump(); Scrollable.ensureVisible( findContext(1), alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtStart, ); await tester.pump(); // 0 and 1 are already visible beyond the top edge, so no change. expect(tester.getBottomLeft(findKey(2)).dy, equals(100.0)); expect(tester.getBottomLeft(findKey(0)).dy, equals(500.0)); Scrollable.ensureVisible( findContext(2), alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtStart, ); await tester.pump(); // Since it is reversed, 2 should have come into view at the top // edge of the scrollable, matching the alignment expectation. expect(tester.getBottomLeft(findKey(2)).dy, equals(300.0)); expect(tester.getBottomLeft(findKey(0)).dy, equals(700.0)); // Bring 0 back into view at the trailing edge, checking the other // alignment. Scrollable.ensureVisible( findContext(0), alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtEnd, ); await tester.pump(); expect(tester.getBottomLeft(findKey(2)).dy, equals(100.0)); expect(tester.getBottomLeft(findKey(0)).dy, equals(500.0)); }); testWidgets('ListView ensureVisible Axis.horizontal reverse', (WidgetTester tester) async { BuildContext findContext(int i) => tester.element(findKey(i)); Future<void> prepare(double offset) async { tester.state<ScrollableState>(find.byType(Scrollable)).position.jumpTo(offset); await tester.pump(); } await tester.pumpWidget(buildListView(Axis.horizontal, reverse: true, shrinkWrap: true)); await prepare(211.0); Scrollable.ensureVisible(findContext(3)); await tester.pump(); expect(tester.getBottomRight(findKey(3)).dx, equals(700.0)); await prepare(23.0); Scrollable.ensureVisible(findContext(0)); await tester.pump(); expect(tester.getBottomRight(findKey(0)).dx, equals(700.0)); await prepare(230.0); Scrollable.ensureVisible(findContext(2), alignment: 1.0); await tester.pump(); expect(tester.getTopLeft(findKey(2)).dx, equals(100.0)); await prepare(1083.0); Scrollable.ensureVisible(findContext(6), alignment: 1.0); await tester.pump(); expect(tester.getBottomRight(findKey(6)).dx, equals(300.0)); await prepare(345.0); Scrollable.ensureVisible(findContext(3), duration: const Duration(seconds: 1)); await tester.pump(); await tester.pump(const Duration(milliseconds: 1020)); expect(tester.getBottomRight(findKey(3)).dx, equals(700.0)); // Regression test for https://github.com/flutter/flutter/issues/128749 // Reset to zero position. await prepare(0.0); // 3 is not currently visible as the ListView is contained // within a centered SizedBox. expect(tester.getBottomLeft(findKey(3)).dx, equals(-100.0)); expect(tester.getBottomLeft(findKey(0)).dx, equals(500.0)); Scrollable.ensureVisible( findContext(0), alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtStart, ); await tester.pump(); Scrollable.ensureVisible( findContext(1), alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtStart, ); await tester.pump(); Scrollable.ensureVisible( findContext(2), alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtStart, ); await tester.pump(); // 0, 1 and 2 are already visible beyond the left edge, so no change. expect(tester.getBottomLeft(findKey(3)).dx, equals(-100.0)); expect(tester.getBottomLeft(findKey(0)).dx, equals(500.0)); Scrollable.ensureVisible( findContext(3), alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtStart, ); await tester.pump(); // Since it is reversed, 3 should have come into view at the leading // edge of the scrollable, matching the alignment expectation. expect(tester.getBottomLeft(findKey(3)).dx, equals(100.0)); expect(tester.getBottomLeft(findKey(0)).dx, equals(700.0)); // Bring 0 back into view at the trailing edge, checking the other // alignment. Scrollable.ensureVisible( findContext(0), alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtEnd, ); await tester.pump(); expect(tester.getBottomLeft(findKey(3)).dx, equals(-100.0)); expect(tester.getBottomLeft(findKey(0)).dx, equals(500.0)); }); }); group('Scrollable with center', () { testWidgets('ensureVisible', (WidgetTester tester) async { BuildContext findContext(int i) => tester.element(findKey(i)); Future<void> prepare(double offset) async { tester.state<ScrollableState>(find.byType(Scrollable)).position.jumpTo(offset); await tester.pump(); } await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Center( child: SizedBox( width: 600.0, height: 400.0, child: Scrollable( viewportBuilder: (BuildContext context, ViewportOffset offset) { return Viewport( offset: offset, center: const ValueKey<String>('center'), slivers: const <Widget>[ SliverToBoxAdapter(child: SizedBox(key: ValueKey<int>(-6), width: 200.0, height: 200.0)), SliverToBoxAdapter(child: SizedBox(key: ValueKey<int>(-5), width: 200.0, height: 200.0)), SliverToBoxAdapter(child: SizedBox(key: ValueKey<int>(-4), width: 200.0, height: 200.0)), SliverToBoxAdapter(child: SizedBox(key: ValueKey<int>(-3), width: 200.0, height: 200.0)), SliverToBoxAdapter(child: SizedBox(key: ValueKey<int>(-2), width: 200.0, height: 200.0)), SliverToBoxAdapter(child: SizedBox(key: ValueKey<int>(-1), width: 200.0, height: 200.0)), SliverToBoxAdapter(key: ValueKey<String>('center'), child: SizedBox(key: ValueKey<int>(0), width: 200.0, height: 200.0)), SliverToBoxAdapter(child: SizedBox(key: ValueKey<int>(1), width: 200.0, height: 200.0)), SliverToBoxAdapter(child: SizedBox(key: ValueKey<int>(2), width: 200.0, height: 200.0)), SliverToBoxAdapter(child: SizedBox(key: ValueKey<int>(3), width: 200.0, height: 200.0)), SliverToBoxAdapter(child: SizedBox(key: ValueKey<int>(4), width: 200.0, height: 200.0)), SliverToBoxAdapter(child: SizedBox(key: ValueKey<int>(5), width: 200.0, height: 200.0)), SliverToBoxAdapter(child: SizedBox(key: ValueKey<int>(6), width: 200.0, height: 200.0)), ], ); }, ), ), ), ), ); await prepare(480.0); Scrollable.ensureVisible(findContext(3)); await tester.pump(); expect(tester.getTopLeft(findKey(3)).dy, equals(100.0)); await prepare(1083.0); Scrollable.ensureVisible(findContext(6)); await tester.pump(); expect(tester.getTopLeft(findKey(6)).dy, equals(300.0)); await prepare(735.0); Scrollable.ensureVisible(findContext(4), alignment: 1.0); await tester.pump(); expect(tester.getBottomRight(findKey(4)).dy, equals(500.0)); await prepare(123.0); Scrollable.ensureVisible(findContext(0), alignment: 1.0); await tester.pump(); expect(tester.getBottomRight(findKey(0)).dy, equals(500.0)); await prepare(523.0); Scrollable.ensureVisible(findContext(3), duration: const Duration(seconds: 1)); await tester.pump(); await tester.pump(const Duration(milliseconds: 1020)); expect(tester.getTopLeft(findKey(3)).dy, equals(100.0)); await prepare(-480.0); Scrollable.ensureVisible(findContext(-3)); await tester.pump(); expect(tester.getTopLeft(findKey(-3)).dy, equals(100.0)); await prepare(-1083.0); Scrollable.ensureVisible(findContext(-6)); await tester.pump(); expect(tester.getTopLeft(findKey(-6)).dy, equals(100.0)); await prepare(-735.0); Scrollable.ensureVisible(findContext(-4), alignment: 1.0); await tester.pump(); expect(tester.getBottomRight(findKey(-4)).dy, equals(500.0)); await prepare(-523.0); Scrollable.ensureVisible(findContext(-3), duration: const Duration(seconds: 1)); await tester.pump(); await tester.pump(const Duration(milliseconds: 1020)); expect(tester.getTopLeft(findKey(-3)).dy, equals(100.0)); }); }); group('TwoDimensionalViewport ensureVisible', () { Finder findKey(ChildVicinity vicinity) { return find.byKey(ValueKey<ChildVicinity>(vicinity)); } BuildContext findContext(WidgetTester tester, ChildVicinity vicinity) { return tester.element(findKey(vicinity)); } testWidgets('Axis.vertical', (WidgetTester tester) async { await tester.pumpWidget(simpleBuilderTest(useCacheExtent: true)); Scrollable.ensureVisible(findContext( tester, const ChildVicinity(xIndex: 0, yIndex: 0)), ); await tester.pump(); expect( tester.getTopLeft(findKey(const ChildVicinity(xIndex: 0, yIndex: 0))).dy, equals(0.0), ); // (0, 3) is in the cache extent, and will be brought into view next expect( tester.getTopLeft(findKey(const ChildVicinity(xIndex: 0, yIndex: 3))).dy, equals(600.0), ); Scrollable.ensureVisible(findContext( tester, const ChildVicinity(xIndex: 0, yIndex: 3)), ); await tester.pump(); // Now in view at top edge of viewport expect( tester.getTopLeft(findKey(const ChildVicinity(xIndex: 0, yIndex: 3))).dy, equals(0.0), ); // If already visible, no change Scrollable.ensureVisible(findContext( tester, const ChildVicinity(xIndex: 0, yIndex: 3)), ); await tester.pump(); expect( tester.getTopLeft(findKey(const ChildVicinity(xIndex: 0, yIndex: 3))).dy, equals(0.0), ); }); testWidgets('Axis.horizontal', (WidgetTester tester) async { await tester.pumpWidget(simpleBuilderTest(useCacheExtent: true)); Scrollable.ensureVisible(findContext( tester, const ChildVicinity(xIndex: 1, yIndex: 0)), ); await tester.pump(); expect( tester.getTopLeft(findKey(const ChildVicinity(xIndex: 1, yIndex: 0))).dx, equals(0.0), ); // (5, 0) is now in the cache extent, and will be brought into view next expect( tester.getTopLeft(findKey(const ChildVicinity(xIndex: 5, yIndex: 0))).dx, equals(800.0), ); Scrollable.ensureVisible(findContext( tester, const ChildVicinity(xIndex: 5, yIndex: 0)), alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtEnd, ); await tester.pump(); // Now in view at trailing edge of viewport expect( tester.getTopLeft(findKey(const ChildVicinity(xIndex: 5, yIndex: 0))).dx, equals(600.0), ); // If already in position, no change Scrollable.ensureVisible(findContext( tester, const ChildVicinity(xIndex: 5, yIndex: 0)), alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtEnd, ); await tester.pump(); expect( tester.getTopLeft(findKey(const ChildVicinity(xIndex: 5, yIndex: 0))).dx, equals(600.0), ); }); testWidgets('both axes', (WidgetTester tester) async { await tester.pumpWidget(simpleBuilderTest(useCacheExtent: true)); Scrollable.ensureVisible(findContext( tester, const ChildVicinity(xIndex: 1, yIndex: 1)), ); await tester.pump(); expect( tester.getRect(findKey(const ChildVicinity(xIndex: 1, yIndex: 1))), const Rect.fromLTRB(0.0, 0.0, 200.0, 200.0), ); // (5, 4) is in the cache extent, and will be brought into view next expect( tester.getRect(findKey(const ChildVicinity(xIndex: 5, yIndex: 4))), const Rect.fromLTRB(800.0, 600.0, 1000.0, 800.0), ); Scrollable.ensureVisible(findContext( tester, const ChildVicinity(xIndex: 5, yIndex: 4)), alignment: 1.0, // Same as ScrollAlignmentPolicy.keepVisibleAtEnd ); await tester.pump(); // Now in view at bottom trailing corner of viewport expect( tester.getRect(findKey(const ChildVicinity(xIndex: 5, yIndex: 4))), const Rect.fromLTRB(600.0, 400.0, 800.0, 600.0), ); // If already visible, no change Scrollable.ensureVisible(findContext( tester, const ChildVicinity(xIndex: 5, yIndex: 4)), alignment: 1.0, ); await tester.pump(); expect( tester.getRect(findKey(const ChildVicinity(xIndex: 5, yIndex: 4))), const Rect.fromLTRB(600.0, 400.0, 800.0, 600.0), ); }); testWidgets('Axis.vertical reverse', (WidgetTester tester) async { await tester.pumpWidget(simpleBuilderTest( verticalDetails: const ScrollableDetails.vertical(reverse: true), useCacheExtent: true, )); expect( tester.getTopLeft(findKey(const ChildVicinity(xIndex: 0, yIndex: 0))).dy, equals(400.0), ); Scrollable.ensureVisible(findContext( tester, const ChildVicinity(xIndex: 0, yIndex: 0)), ); await tester.pump(); // Already visible so no change. expect( tester.getTopLeft(findKey(const ChildVicinity(xIndex: 0, yIndex: 0))).dy, equals(400.0), ); // (0, 3) is in the cache extent, and will be brought into view next expect( tester.getTopLeft(findKey(const ChildVicinity(xIndex: 0, yIndex: 3))).dy, equals(-200.0), ); Scrollable.ensureVisible(findContext( tester, const ChildVicinity(xIndex: 0, yIndex: 3)), ); await tester.pump(); // Now in view at bottom edge of viewport since we are reversed expect( tester.getTopLeft(findKey(const ChildVicinity(xIndex: 0, yIndex: 3))).dy, equals(400.0), ); // If already visible, no change Scrollable.ensureVisible(findContext( tester, const ChildVicinity(xIndex: 0, yIndex: 3)), ); await tester.pump(); expect( tester.getTopLeft(findKey(const ChildVicinity(xIndex: 0, yIndex: 3))).dy, equals(400.0), ); }); testWidgets('Axis.horizontal reverse', (WidgetTester tester) async { await tester.pumpWidget(simpleBuilderTest( horizontalDetails: const ScrollableDetails.horizontal(reverse: true), useCacheExtent: true, )); expect( tester.getTopLeft(findKey(const ChildVicinity(xIndex: 0, yIndex: 0))).dx, equals(600.0), ); Scrollable.ensureVisible(findContext( tester, const ChildVicinity(xIndex: 0, yIndex: 0)), ); await tester.pump(); // Already visible so no change. expect( tester.getTopLeft(findKey(const ChildVicinity(xIndex: 0, yIndex: 0))).dx, equals(600.0), ); // (4, 0) is in the cache extent, and will be brought into view next expect( tester.getTopLeft(findKey(const ChildVicinity(xIndex: 4, yIndex: 0))).dx, equals(-200.0), ); Scrollable.ensureVisible(findContext( tester, const ChildVicinity(xIndex: 4, yIndex: 0)), ); await tester.pump(); expect( tester.getTopLeft(findKey(const ChildVicinity(xIndex: 4, yIndex: 0))).dx, equals(200.0), ); // If already visible, no change Scrollable.ensureVisible(findContext( tester, const ChildVicinity(xIndex: 4, yIndex: 0)), ); await tester.pump(); expect( tester.getTopLeft(findKey(const ChildVicinity(xIndex: 4, yIndex: 0))).dx, equals(200.0), ); }); testWidgets('both axes reverse', (WidgetTester tester) async { await tester.pumpWidget(simpleBuilderTest( verticalDetails: const ScrollableDetails.vertical(reverse: true), horizontalDetails: const ScrollableDetails.horizontal(reverse: true), useCacheExtent: true, )); Scrollable.ensureVisible(findContext( tester, const ChildVicinity(xIndex: 1, yIndex: 1)), ); await tester.pump(); expect( tester.getRect(findKey(const ChildVicinity(xIndex: 1, yIndex: 1))), const Rect.fromLTRB(600.0, 400.0, 800.0, 600.0), ); // (5, 4) is in the cache extent, and will be brought into view next expect( tester.getRect(findKey(const ChildVicinity(xIndex: 5, yIndex: 4))), const Rect.fromLTRB(-200.0, -200.0, 0.0, 0.0), ); Scrollable.ensureVisible(findContext( tester, const ChildVicinity(xIndex: 5, yIndex: 4)), alignment: 1.0, // Same as ScrollAlignmentPolicy.keepVisibleAtEnd ); await tester.pump(); // Now in view at trailing corner of viewport expect( tester.getRect(findKey(const ChildVicinity(xIndex: 5, yIndex: 4))), const Rect.fromLTRB(0.0, 0.0, 200.0, 200.0), ); // If already visible, no change Scrollable.ensureVisible(findContext( tester, const ChildVicinity(xIndex: 5, yIndex: 4)), alignment: 1.0, ); await tester.pump(); expect( tester.getRect(findKey(const ChildVicinity(xIndex: 5, yIndex: 4))), const Rect.fromLTRB(0.0, 0.0, 200.0, 200.0), ); }); }); }