blob: 67b98975247e46eb68ae7e39339c9caf45691a36 [file] [log] [blame]
// 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 separate from viewport_caching_test.dart because we can't use
// both testWidgets and rendering_tester in the same file - testWidgets will
// initialize a binding, which rendering_tester will attempt to re-initialize
// (or vice versa).
@Tags(<String>['reduced-test-set'])
import 'dart:ui' as ui;
import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
class _TestSliverPersistentHeaderDelegate extends SliverPersistentHeaderDelegate {
_TestSliverPersistentHeaderDelegate({
this.key,
required this.minExtent,
required this.maxExtent,
this.vsync = const TestVSync(),
this.showOnScreenConfiguration = const PersistentHeaderShowOnScreenConfiguration(),
});
final Key? key;
@override
final double maxExtent;
@override
final double minExtent;
@override
final TickerProvider? vsync;
@override
final PersistentHeaderShowOnScreenConfiguration showOnScreenConfiguration;
@override
Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) => SizedBox.expand(key: key);
@override
bool shouldRebuild(_TestSliverPersistentHeaderDelegate oldDelegate) => true;
}
void main() {
testWidgets('Scrollable widget scrollDirection update test', (WidgetTester tester) async {
final ScrollController controller = ScrollController();
Widget buildFrame(Axis axis) {
return Directionality(
textDirection: TextDirection.ltr,
child: Center(
child: SizedBox(
height: 100.0,
width: 100.0,
child: SingleChildScrollView(
controller: controller,
scrollDirection: axis,
child: const SizedBox(
width: 200,
height: 200,
child: SizedBox.shrink(),
),
),
),
),
);
}
await tester.pumpWidget(buildFrame(Axis.vertical));
expect(controller.position.pixels, 0.0);
// Change the SingleChildScrollView.scrollDirection to horizontal.
await tester.pumpWidget(buildFrame(Axis.horizontal));
expect(controller.position.pixels, 0.0);
final TestGesture gesture = await tester.startGesture(const Offset(400.0, 300.0));
// Drag in the vertical direction should not cause scrolling.
await gesture.moveBy(const Offset(0.0, 10.0));
expect(controller.position.pixels, 0.0);
await gesture.moveBy(const Offset(0.0, -10.0));
expect(controller.position.pixels, 0.0);
// Drag in the horizontal direction should cause scrolling.
await gesture.moveBy(const Offset(-10.0, 0.0));
expect(controller.position.pixels, 10.0);
await gesture.moveBy(const Offset(10.0, 0.0));
expect(controller.position.pixels, 0.0);
});
testWidgets('Viewport getOffsetToReveal - down', (WidgetTester tester) async {
List<Widget> children;
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Center(
child: SizedBox(
height: 200.0,
width: 300.0,
child: ListView(
controller: ScrollController(initialScrollOffset: 300.0),
children: children = List<Widget>.generate(20, (int i) {
return SizedBox(
height: 100.0,
width: 300.0,
child: Text('Tile $i'),
);
}),
),
),
),
),
);
final RenderAbstractViewport viewport = tester.allRenderObjects.whereType<RenderAbstractViewport>().first;
final RenderObject target = tester.renderObject(find.byWidget(children[5], skipOffstage: false));
RevealedOffset revealed = viewport.getOffsetToReveal(target, 0.0);
expect(revealed.offset, 500.0);
expect(revealed.rect, const Rect.fromLTWH(0.0, 0.0, 300.0, 100.0));
revealed = viewport.getOffsetToReveal(target, 1.0);
expect(revealed.offset, 400.0);
expect(revealed.rect, const Rect.fromLTWH(0.0, 100.0, 300.0, 100.0));
revealed = viewport.getOffsetToReveal(target, 0.0, rect: const Rect.fromLTWH(40.0, 40.0, 10.0, 10.0));
expect(revealed.offset, 540.0);
expect(revealed.rect, const Rect.fromLTWH(40.0, 0.0, 10.0, 10.0));
revealed = viewport.getOffsetToReveal(target, 1.0, rect: const Rect.fromLTWH(40.0, 40.0, 10.0, 10.0));
expect(revealed.offset, 350.0);
expect(revealed.rect, const Rect.fromLTWH(40.0, 190.0, 10.0, 10.0));
});
testWidgets('Viewport getOffsetToReveal - right', (WidgetTester tester) async {
List<Widget> children;
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Center(
child: SizedBox(
height: 300.0,
width: 200.0,
child: ListView(
scrollDirection: Axis.horizontal,
controller: ScrollController(initialScrollOffset: 300.0),
children: children = List<Widget>.generate(20, (int i) {
return SizedBox(
height: 300.0,
width: 100.0,
child: Text('Tile $i'),
);
}),
),
),
),
),
);
final RenderAbstractViewport viewport = tester.allRenderObjects.whereType<RenderAbstractViewport>().first;
final RenderObject target = tester.renderObject(find.byWidget(children[5], skipOffstage: false));
RevealedOffset revealed = viewport.getOffsetToReveal(target, 0.0);
expect(revealed.offset, 500.0);
expect(revealed.rect, const Rect.fromLTWH(0.0, 0.0, 100.0, 300.0));
revealed = viewport.getOffsetToReveal(target, 1.0);
expect(revealed.offset, 400.0);
expect(revealed.rect, const Rect.fromLTWH(100.0, 0.0, 100.0, 300.0));
revealed = viewport.getOffsetToReveal(target, 0.0, rect: const Rect.fromLTWH(40.0, 40.0, 10.0, 10.0));
expect(revealed.offset, 540.0);
expect(revealed.rect, const Rect.fromLTWH(0.0, 40.0, 10.0, 10.0));
revealed = viewport.getOffsetToReveal(target, 1.0, rect: const Rect.fromLTWH(40.0, 40.0, 10.0, 10.0));
expect(revealed.offset, 350.0);
expect(revealed.rect, const Rect.fromLTWH(190.0, 40.0, 10.0, 10.0));
});
testWidgets('Viewport getOffsetToReveal - up', (WidgetTester tester) async {
List<Widget> children;
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Center(
child: SizedBox(
height: 200.0,
width: 300.0,
child: ListView(
controller: ScrollController(initialScrollOffset: 300.0),
reverse: true,
children: children = List<Widget>.generate(20, (int i) {
return SizedBox(
height: 100.0,
width: 300.0,
child: Text('Tile $i'),
);
}),
),
),
),
),
);
final RenderAbstractViewport viewport = tester.allRenderObjects.whereType<RenderAbstractViewport>().first;
final RenderObject target = tester.renderObject(find.byWidget(children[5], skipOffstage: false));
RevealedOffset revealed = viewport.getOffsetToReveal(target, 0.0);
expect(revealed.offset, 500.0);
expect(revealed.rect, const Rect.fromLTWH(0.0, 100.0, 300.0, 100.0));
revealed = viewport.getOffsetToReveal(target, 1.0);
expect(revealed.offset, 400.0);
expect(revealed.rect, const Rect.fromLTWH(0.0, 0.0, 300.0, 100.0));
revealed = viewport.getOffsetToReveal(target, 0.0, rect: const Rect.fromLTWH(40.0, 40.0, 10.0, 10.0));
expect(revealed.offset, 550.0);
expect(revealed.rect, const Rect.fromLTWH(40.0, 190.0, 10.0, 10.0));
revealed = viewport.getOffsetToReveal(target, 1.0, rect: const Rect.fromLTWH(40.0, 40.0, 10.0, 10.0));
expect(revealed.offset, 360.0);
expect(revealed.rect, const Rect.fromLTWH(40.0, 0.0, 10.0, 10.0));
});
testWidgets('Viewport getOffsetToReveal - left', (WidgetTester tester) async {
List<Widget> children;
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Center(
child: SizedBox(
height: 300.0,
width: 200.0,
child: ListView(
scrollDirection: Axis.horizontal,
reverse: true,
controller: ScrollController(initialScrollOffset: 300.0),
children: children = List<Widget>.generate(20, (int i) {
return SizedBox(
height: 300.0,
width: 100.0,
child: Text('Tile $i'),
);
}),
),
),
),
),
);
final RenderAbstractViewport viewport = tester.allRenderObjects.whereType<RenderAbstractViewport>().first;
final RenderObject target = tester.renderObject(find.byWidget(children[5], skipOffstage: false));
RevealedOffset revealed = viewport.getOffsetToReveal(target, 0.0);
expect(revealed.offset, 500.0);
expect(revealed.rect, const Rect.fromLTWH(100.0, 0.0, 100.0, 300.0));
revealed = viewport.getOffsetToReveal(target, 1.0);
expect(revealed.offset, 400.0);
expect(revealed.rect, const Rect.fromLTWH(0.0, 0.0, 100.0, 300.0));
revealed = viewport.getOffsetToReveal(target, 0.0, rect: const Rect.fromLTWH(40.0, 40.0, 10.0, 10.0));
expect(revealed.offset, 550.0);
expect(revealed.rect, const Rect.fromLTWH(190.0, 40.0, 10.0, 10.0));
revealed = viewport.getOffsetToReveal(target, 1.0, rect: const Rect.fromLTWH(40.0, 40.0, 10.0, 10.0));
expect(revealed.offset, 360.0);
expect(revealed.rect, const Rect.fromLTWH(0.0, 40.0, 10.0, 10.0));
});
testWidgets('Viewport getOffsetToReveal Sliver - down', (WidgetTester tester) async {
final List<Widget> children = <Widget>[];
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Center(
child: SizedBox(
height: 200.0,
width: 300.0,
child: CustomScrollView(
controller: ScrollController(initialScrollOffset: 300.0),
slivers: List<Widget>.generate(20, (int i) {
final Widget sliver = SliverToBoxAdapter(
child: SizedBox(
height: 100.0,
child: Text('Tile $i'),
),
);
children.add(sliver);
return SliverPadding(
padding: const EdgeInsets.only(top: 22.0, bottom: 23.0),
sliver: sliver,
);
}),
),
),
),
),
);
final RenderAbstractViewport viewport = tester.allRenderObjects.whereType<RenderAbstractViewport>().first;
final RenderObject target = tester.renderObject(find.byWidget(children[5], skipOffstage: false));
RevealedOffset revealed = viewport.getOffsetToReveal(target, 0.0);
expect(revealed.offset, 5 * (100 + 22 + 23) + 22);
revealed = viewport.getOffsetToReveal(target, 1.0);
expect(revealed.offset, 5 * (100 + 22 + 23) + 22 - 100);
// With rect specified.
revealed = viewport.getOffsetToReveal(target, 0.0, rect: const Rect.fromLTRB(1, 2, 3, 4));
expect(revealed.offset, 5 * (100 + 22 + 23) + 22 + 2);
revealed = viewport.getOffsetToReveal(target, 1.0, rect: const Rect.fromLTRB(1, 2, 3, 4));
expect(revealed.offset, 5 * (100 + 22 + 23) + 22 - (200 - 4));
});
testWidgets('Viewport getOffsetToReveal Sliver - right', (WidgetTester tester) async {
final List<Widget> children = <Widget>[];
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Center(
child: SizedBox(
height: 300.0,
width: 200.0,
child: CustomScrollView(
scrollDirection: Axis.horizontal,
controller: ScrollController(initialScrollOffset: 300.0),
slivers: List<Widget>.generate(20, (int i) {
final Widget sliver = SliverToBoxAdapter(
child: SizedBox(
width: 100.0,
child: Text('Tile $i'),
),
);
children.add(sliver);
return SliverPadding(
padding: const EdgeInsets.only(left: 22.0, right: 23.0),
sliver: sliver,
);
}),
),
),
),
),
);
final RenderAbstractViewport viewport = tester.allRenderObjects.whereType<RenderAbstractViewport>().first;
final RenderObject target = tester.renderObject(find.byWidget(children[5], skipOffstage: false));
RevealedOffset revealed = viewport.getOffsetToReveal(target, 0.0);
expect(revealed.offset, 5 * (100 + 22 + 23) + 22);
revealed = viewport.getOffsetToReveal(target, 1.0);
expect(revealed.offset, 5 * (100 + 22 + 23) + 22 - 100);
// With rect specified.
revealed = viewport.getOffsetToReveal(target, 0.0, rect: const Rect.fromLTRB(1, 2, 3, 4));
expect(revealed.offset, 5 * (100 + 22 + 23) + 22 + 1);
revealed = viewport.getOffsetToReveal(target, 1.0, rect: const Rect.fromLTRB(1, 2, 3, 4));
expect(revealed.offset, 5 * (100 + 22 + 23) + 22 - (200 - 3));
});
testWidgets('Viewport getOffsetToReveal Sliver - up', (WidgetTester tester) async {
final List<Widget> children = <Widget>[];
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Center(
child: SizedBox(
height: 200.0,
width: 300.0,
child: CustomScrollView(
controller: ScrollController(initialScrollOffset: 300.0),
reverse: true,
slivers: List<Widget>.generate(20, (int i) {
final Widget sliver = SliverToBoxAdapter(
child: SizedBox(
height: 100.0,
child: Text('Tile $i'),
),
);
children.add(sliver);
return SliverPadding(
padding: const EdgeInsets.only(top: 22.0, bottom: 23.0),
sliver: sliver,
);
}),
),
),
),
),
);
final RenderAbstractViewport viewport = tester.allRenderObjects.whereType<RenderAbstractViewport>().first;
final RenderObject target = tester.renderObject(find.byWidget(children[5], skipOffstage: false));
RevealedOffset revealed = viewport.getOffsetToReveal(target, 0.0);
// Does not include the bottom padding of children[5] thus + 23 instead of + 22.
expect(revealed.offset, 5 * (100 + 22 + 23) + 23);
revealed = viewport.getOffsetToReveal(target, 1.0);
expect(revealed.offset, 5 * (100 + 22 + 23) + 23 - 100);
// With rect specified.
revealed = viewport.getOffsetToReveal(target, 0.0, rect: const Rect.fromLTRB(1, 2, 3, 4));
expect(revealed.offset, 5 * (100 + 22 + 23) + 23 + (100 - 4));
revealed = viewport.getOffsetToReveal(target, 1.0, rect: const Rect.fromLTRB(1, 2, 3, 4));
expect(revealed.offset, - 200 + 6 * (100 + 22 + 23) - 22 - 2);
});
testWidgets('Viewport getOffsetToReveal Sliver - up - reverse growth', (WidgetTester tester) async {
const Key centerKey = ValueKey<String>('center');
const EdgeInsets padding = EdgeInsets.only(top: 22.0, bottom: 23.0);
const Widget centerSliver = SliverPadding(
key: centerKey,
padding: padding,
sliver: SliverToBoxAdapter(
child: SizedBox(
height: 100.0,
child: Text('Tile center'),
),
),
);
const Widget lowerItem = SizedBox(
height: 100.0,
child: Text('Tile lower'),
);
const Widget lowerSliver = SliverPadding(
padding: padding,
sliver: SliverToBoxAdapter(
child: lowerItem,
),
);
await tester.pumpWidget(
const Directionality(
textDirection: TextDirection.ltr,
child: Center(
child: SizedBox(
height: 200.0,
width: 300.0,
child: CustomScrollView(
center: centerKey,
reverse: true,
slivers: <Widget>[lowerSliver, centerSliver],
),
),
),
),
);
final RenderAbstractViewport viewport = tester.allRenderObjects.whereType<RenderAbstractViewport>().first;
final RenderObject target = tester.renderObject(find.byWidget(lowerItem, skipOffstage: false));
RevealedOffset revealed = viewport.getOffsetToReveal(target, 0.0);
expect(revealed.offset, - 100 - 22);
revealed = viewport.getOffsetToReveal(target, 1.0);
expect(revealed.offset, - 100 - 22 - 100);
// With rect specified.
revealed = viewport.getOffsetToReveal(target, 0.0, rect: const Rect.fromLTRB(1, 2, 3, 4));
expect(revealed.offset, - 22 - 4);
revealed = viewport.getOffsetToReveal(target, 1.0, rect: const Rect.fromLTRB(1, 2, 3, 4));
expect(revealed.offset, -200 - 22 - 2);
});
testWidgets('Viewport getOffsetToReveal Sliver - left - reverse growth', (WidgetTester tester) async {
const Key centerKey = ValueKey<String>('center');
const EdgeInsets padding = EdgeInsets.only(left: 22.0, right: 23.0);
const Widget centerSliver = SliverPadding(
key: centerKey,
padding: padding,
sliver: SliverToBoxAdapter(
child: SizedBox(
width: 100.0,
child: Text('Tile center'),
),
),
);
const Widget lowerItem = SizedBox(
width: 100.0,
child: Text('Tile lower'),
);
const Widget lowerSliver = SliverPadding(
padding: padding,
sliver: SliverToBoxAdapter(
child: lowerItem,
),
);
await tester.pumpWidget(
const Directionality(
textDirection: TextDirection.ltr,
child: Center(
child: SizedBox(
height: 200.0,
width: 300.0,
child: CustomScrollView(
scrollDirection: Axis.horizontal,
center: centerKey,
reverse: true,
slivers: <Widget>[lowerSliver, centerSliver],
),
),
),
),
);
final RenderAbstractViewport viewport = tester.allRenderObjects.whereType<RenderAbstractViewport>().first;
final RenderObject target = tester.renderObject(find.byWidget(lowerItem, skipOffstage: false));
RevealedOffset revealed = viewport.getOffsetToReveal(target, 0.0);
expect(revealed.offset, -100 - 22);
revealed = viewport.getOffsetToReveal(target, 1.0);
expect(revealed.offset, - 100 - 22 - 200);
// With rect specified.
revealed = viewport.getOffsetToReveal(target, 0.0, rect: const Rect.fromLTRB(1, 2, 3, 4));
expect(revealed.offset, - 22 - 3);
revealed = viewport.getOffsetToReveal(target, 1.0, rect: const Rect.fromLTRB(1, 2, 3, 4));
expect(revealed.offset, - 300 - 22 - 1);
});
testWidgets('Viewport getOffsetToReveal Sliver - left', (WidgetTester tester) async {
final List<Widget> children = <Widget>[];
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Center(
child: SizedBox(
height: 300.0,
width: 200.0,
child: CustomScrollView(
scrollDirection: Axis.horizontal,
reverse: true,
controller: ScrollController(initialScrollOffset: 300.0),
slivers: List<Widget>.generate(20, (int i) {
final Widget sliver = SliverToBoxAdapter(
child: SizedBox(
width: 100.0,
child: Text('Tile $i'),
),
);
children.add(sliver);
return SliverPadding(
padding: const EdgeInsets.only(left: 22.0, right: 23.0),
sliver: sliver,
);
}),
),
),
),
),
);
final RenderAbstractViewport viewport = tester.allRenderObjects.whereType<RenderAbstractViewport>().first;
final RenderObject target = tester.renderObject(find.byWidget(children[5], skipOffstage: false));
RevealedOffset revealed = viewport.getOffsetToReveal(target, 0.0);
expect(revealed.offset, 5 * (100 + 22 + 23) + 23);
revealed = viewport.getOffsetToReveal(target, 1.0);
expect(revealed.offset, 5 * (100 + 22 + 23) + 23 - 100);
// With rect specified.
revealed = viewport.getOffsetToReveal(target, 0.0, rect: const Rect.fromLTRB(1, 2, 3, 4));
expect(revealed.offset, 6 * (100 + 22 + 23) - 22 - 3);
revealed = viewport.getOffsetToReveal(target, 1.0, rect: const Rect.fromLTRB(1, 2, 3, 4));
expect(revealed.offset, -200 + 6 * (100 + 22 + 23) - 22 - 1);
});
testWidgets('Nested Viewports showOnScreen', (WidgetTester tester) async {
final List<ScrollController> controllersX = List<ScrollController>.generate(10, (int i) => ScrollController(initialScrollOffset: 400.0));
final ScrollController controllerY = ScrollController(initialScrollOffset: 400.0);
final List<List<Widget>> children = List<List<Widget>>.generate(10, (int y) {
return List<Widget>.generate(10, (int x) {
return SizedBox(
height: 100.0,
width: 100.0,
child: Text('$x,$y'),
);
});
});
/// Builds a grid:
///
/// <- x ->
/// 0 1 2 3 4 5 6 7 8 9
/// 0 c c c c c c c c c c
/// 1 c c c c c c c c c c
/// 2 c c c c c c c c c c
/// 3 c c c c c c c c c c y
/// 4 c c c c v v c c c c
/// 5 c c c c v v c c c c
/// 6 c c c c c c c c c c
/// 7 c c c c c c c c c c
/// 8 c c c c c c c c c c
/// 9 c c c c c c c c c c
///
/// Each c is a 100x100 container, v are containers visible in initial
/// viewport.
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Center(
child: SizedBox(
height: 200.0,
width: 200.0,
child: ListView(
controller: controllerY,
children: List<Widget>.generate(10, (int y) {
return SizedBox(
height: 100.0,
child: ListView(
scrollDirection: Axis.horizontal,
controller: controllersX[y],
children: children[y],
),
);
}),
),
),
),
),
);
// Already in viewport
tester.renderObject(find.byWidget(children[4][4], skipOffstage: false)).showOnScreen();
await tester.pumpAndSettle();
expect(controllersX[4].offset, 400.0);
expect(controllerY.offset, 400.0);
controllersX[4].jumpTo(400.0);
controllerY.jumpTo(400.0);
await tester.pumpAndSettle();
// Above viewport
tester.renderObject(find.byWidget(children[3][4], skipOffstage: false)).showOnScreen();
await tester.pumpAndSettle();
expect(controllersX[3].offset, 400.0);
expect(controllerY.offset, 300.0);
controllersX[3].jumpTo(400.0);
controllerY.jumpTo(400.0);
await tester.pumpAndSettle();
// Below viewport
tester.renderObject(find.byWidget(children[6][4], skipOffstage: false)).showOnScreen();
await tester.pumpAndSettle();
expect(controllersX[6].offset, 400.0);
expect(controllerY.offset, 500.0);
controllersX[6].jumpTo(400.0);
controllerY.jumpTo(400.0);
await tester.pumpAndSettle();
// Left of viewport
tester.renderObject(find.byWidget(children[4][3], skipOffstage: false)).showOnScreen();
await tester.pumpAndSettle();
expect(controllersX[4].offset, 300.0);
expect(controllerY.offset, 400.0);
controllersX[4].jumpTo(400.0);
controllerY.jumpTo(400.0);
await tester.pumpAndSettle();
// Right of viewport
tester.renderObject(find.byWidget(children[4][6], skipOffstage: false)).showOnScreen();
await tester.pumpAndSettle();
expect(controllersX[4].offset, 500.0);
expect(controllerY.offset, 400.0);
controllersX[4].jumpTo(400.0);
controllerY.jumpTo(400.0);
await tester.pumpAndSettle();
// Above and left of viewport
tester.renderObject(find.byWidget(children[3][3], skipOffstage: false)).showOnScreen();
await tester.pumpAndSettle();
expect(controllersX[3].offset, 300.0);
expect(controllerY.offset, 300.0);
controllersX[3].jumpTo(400.0);
controllerY.jumpTo(400.0);
await tester.pumpAndSettle();
// Below and left of viewport
tester.renderObject(find.byWidget(children[6][3], skipOffstage: false)).showOnScreen();
await tester.pumpAndSettle();
expect(controllersX[6].offset, 300.0);
expect(controllerY.offset, 500.0);
controllersX[6].jumpTo(400.0);
controllerY.jumpTo(400.0);
await tester.pumpAndSettle();
// Above and right of viewport
tester.renderObject(find.byWidget(children[3][6], skipOffstage: false)).showOnScreen();
await tester.pumpAndSettle();
expect(controllersX[3].offset, 500.0);
expect(controllerY.offset, 300.0);
controllersX[3].jumpTo(400.0);
controllerY.jumpTo(400.0);
await tester.pumpAndSettle();
// Below and right of viewport
tester.renderObject(find.byWidget(children[6][6], skipOffstage: false)).showOnScreen();
await tester.pumpAndSettle();
expect(controllersX[6].offset, 500.0);
expect(controllerY.offset, 500.0);
controllersX[6].jumpTo(400.0);
controllerY.jumpTo(400.0);
await tester.pumpAndSettle();
// Below and right of viewport with animations
tester.renderObject(find.byWidget(children[6][6], skipOffstage: false)).showOnScreen(duration: const Duration(seconds: 2));
await tester.pump();
await tester.pump(const Duration(seconds: 1));
expect(tester.hasRunningAnimations, isTrue);
expect(controllersX[6].offset, greaterThan(400.0));
expect(controllersX[6].offset, lessThan(500.0));
expect(controllerY.offset, greaterThan(400.0));
expect(controllerY.offset, lessThan(500.0));
await tester.pumpAndSettle();
expect(controllersX[6].offset, 500.0);
expect(controllerY.offset, 500.0);
});
group('Nested viewports (same orientation) showOnScreen', () {
final List<Widget> children = List<Widget>.generate(10, (int i) {
return SizedBox(
height: 100.0,
width: 300.0,
child: Text('$i'),
);
});
Future<void> buildNestedScroller({ required WidgetTester tester, required ScrollController inner, required ScrollController outer }) {
return tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Center(
child: SizedBox(
height: 200.0,
width: 300.0,
child: ListView(
controller: outer,
children: <Widget>[
const SizedBox(
height: 200.0,
),
SizedBox(
height: 200.0,
width: 300.0,
child: ListView(
controller: inner,
children: children,
),
),
const SizedBox(
height: 200.0,
),
],
),
),
),
),
);
}
testWidgets('Reverse List showOnScreen', (WidgetTester tester) async {
final ui.Size originalScreenSize = tester.binding.window.physicalSize;
final double originalDevicePixelRatio = tester.binding.window.devicePixelRatio;
addTearDown(() {
tester.binding.window.devicePixelRatioTestValue = originalDevicePixelRatio;
tester.binding.window.physicalSizeTestValue = originalScreenSize;
});
const double screenHeight = 400.0;
const double screenWidth = 400.0;
const double itemHeight = screenHeight / 10.0;
const ValueKey<String> centerKey = ValueKey<String>('center');
tester.binding.window.devicePixelRatioTestValue = 1.0;
tester.binding.window.physicalSizeTestValue = const Size(screenWidth, screenHeight);
await tester.pumpWidget(Directionality(
textDirection: TextDirection.ltr,
child: CustomScrollView(
center: centerKey,
reverse: true,
slivers: <Widget>[
SliverList(
delegate: SliverChildListDelegate(
List<Widget>.generate(
10,
(int index) => SizedBox(
height: itemHeight,
child: Text('Item ${-index - 1}'),
),
),
),
),
SliverList(
key: centerKey,
delegate: SliverChildListDelegate(
List<Widget>.generate(
1,
(int index) => const SizedBox(
height: itemHeight,
child: Text('Item 0'),
),
),
),
),
SliverList(
delegate: SliverChildListDelegate(
List<Widget>.generate(
10,
(int index) => SizedBox(
height: itemHeight,
child: Text('Item ${index + 1}'),
),
),
),
),
],
),
),
);
expect(find.text('Item -1'), findsNothing);
final RenderBox itemNeg1 =
tester.renderObject(find.text('Item -1', skipOffstage: false));
itemNeg1.showOnScreen(duration: const Duration(seconds: 1));
await tester.pumpAndSettle();
expect(find.text('Item -1'), findsOneWidget);
});
testWidgets('in view in inner, but not in outer', (WidgetTester tester) async {
final ScrollController inner = ScrollController();
final ScrollController outer = ScrollController();
await buildNestedScroller(
tester: tester,
inner: inner,
outer: outer,
);
expect(outer.offset, 0.0);
expect(inner.offset, 0.0);
tester.renderObject(find.byWidget(children[0], skipOffstage: false)).showOnScreen();
await tester.pumpAndSettle();
expect(inner.offset, 0.0);
expect(outer.offset, 100.0);
});
testWidgets('not in view of neither inner nor outer', (WidgetTester tester) async {
final ScrollController inner = ScrollController();
final ScrollController outer = ScrollController();
await buildNestedScroller(
tester: tester,
inner: inner,
outer: outer,
);
expect(outer.offset, 0.0);
expect(inner.offset, 0.0);
tester.renderObject(find.byWidget(children[4], skipOffstage: false)).showOnScreen();
await tester.pumpAndSettle();
expect(inner.offset, 300.0);
expect(outer.offset, 200.0);
});
testWidgets('in view in inner and outer', (WidgetTester tester) async {
final ScrollController inner = ScrollController(initialScrollOffset: 200.0);
final ScrollController outer = ScrollController(initialScrollOffset: 200.0);
await buildNestedScroller(
tester: tester,
inner: inner,
outer: outer,
);
expect(outer.offset, 200.0);
expect(inner.offset, 200.0);
tester.renderObject(find.byWidget(children[2])).showOnScreen();
await tester.pumpAndSettle();
expect(outer.offset, 200.0);
expect(inner.offset, 200.0);
});
testWidgets('inner shown in outer, but item not visible', (WidgetTester tester) async {
final ScrollController inner = ScrollController(initialScrollOffset: 200.0);
final ScrollController outer = ScrollController(initialScrollOffset: 200.0);
await buildNestedScroller(
tester: tester,
inner: inner,
outer: outer,
);
expect(outer.offset, 200.0);
expect(inner.offset, 200.0);
tester.renderObject(find.byWidget(children[5], skipOffstage: false)).showOnScreen();
await tester.pumpAndSettle();
expect(outer.offset, 200.0);
expect(inner.offset, 400.0);
});
testWidgets('inner half shown in outer, item only visible in inner', (WidgetTester tester) async {
final ScrollController inner = ScrollController();
final ScrollController outer = ScrollController(initialScrollOffset: 100.0);
await buildNestedScroller(
tester: tester,
inner: inner,
outer: outer,
);
expect(outer.offset, 100.0);
expect(inner.offset, 0.0);
tester.renderObject(find.byWidget(children[1])).showOnScreen();
await tester.pumpAndSettle();
expect(outer.offset, 200.0);
expect(inner.offset, 0.0);
});
});
testWidgets('Nested Viewports showOnScreen with allowImplicitScrolling=false for inner viewport', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/20893.
List<Widget> slivers;
final ScrollController controllerX = ScrollController();
final ScrollController controllerY = ScrollController();
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Center(
child: SizedBox(
height: 200.0,
width: 200.0,
child: ListView(
controller: controllerY,
children: <Widget>[
const SizedBox(
height: 150.0,
),
SizedBox(
height: 100.0,
child: ListView(
physics: const PageScrollPhysics(), // Turns off `allowImplicitScrolling`
scrollDirection: Axis.horizontal,
controller: controllerX,
children: slivers = <Widget>[
Container(
width: 150.0,
),
Container(
width: 150.0,
),
],
),
),
const SizedBox(
height: 150.0,
),
],
),
),
),
),
);
tester.renderObject(find.byWidget(slivers[1])).showOnScreen();
await tester.pumpAndSettle();
expect(controllerX.offset, 0.0);
expect(controllerY.offset, 50.0);
});
testWidgets('Nested Viewports showOnScreen on Sliver with allowImplicitScrolling=false for inner viewport', (WidgetTester tester) async {
Widget sliver;
final ScrollController controllerX = ScrollController();
final ScrollController controllerY = ScrollController();
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Center(
child: SizedBox(
height: 200.0,
width: 200.0,
child: ListView(
controller: controllerY,
children: <Widget>[
const SizedBox(
height: 150.0,
),
SizedBox(
height: 100.0,
child: CustomScrollView(
physics: const PageScrollPhysics(), // Turns off `allowImplicitScrolling`
scrollDirection: Axis.horizontal,
controller: controllerX,
slivers: <Widget>[
SliverPadding(
padding: const EdgeInsets.all(25.0),
sliver: SliverToBoxAdapter(
child: Container(
width: 100.0,
),
),
),
SliverPadding(
padding: const EdgeInsets.all(25.0),
sliver: sliver = SliverToBoxAdapter(
child: Container(
width: 100.0,
),
),
),
],
),
),
const SizedBox(
height: 150.0,
),
],
),
),
),
),
);
tester.renderObject(find.byWidget(sliver)).showOnScreen();
await tester.pumpAndSettle();
expect(controllerX.offset, 0.0);
expect(controllerY.offset, 25.0);
});
testWidgets('Viewport showOnScreen with objects larger than viewport', (WidgetTester tester) async {
List<Widget> children;
ScrollController controller;
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Center(
child: SizedBox(
height: 200.0,
child: ListView(
controller: controller = ScrollController(initialScrollOffset: 300.0),
children: children = List<Widget>.generate(20, (int i) {
return SizedBox(
height: 300.0,
child: Text('Tile $i'),
);
}),
),
),
),
),
);
expect(controller.offset, 300.0);
// Already aligned with leading edge, nothing happens.
tester.renderObject(find.byWidget(children[1], skipOffstage: false)).showOnScreen();
await tester.pumpAndSettle();
expect(controller.offset, 300.0);
// Above leading edge aligns trailing edges
tester.renderObject(find.byWidget(children[0], skipOffstage: false)).showOnScreen();
await tester.pumpAndSettle();
expect(controller.offset, 100.0);
// Below trailing edge aligns leading edges
tester.renderObject(find.byWidget(children[1], skipOffstage: false)).showOnScreen();
await tester.pumpAndSettle();
expect(controller.offset, 300.0);
controller.jumpTo(250.0);
await tester.pumpAndSettle();
expect(controller.offset, 250.0);
// Partly visible across leading edge aligns trailing edges
tester.renderObject(find.byWidget(children[0], skipOffstage: false)).showOnScreen();
await tester.pumpAndSettle();
expect(controller.offset, 100.0);
controller.jumpTo(150.0);
await tester.pumpAndSettle();
expect(controller.offset, 150.0);
// Partly visible across trailing edge aligns leading edges
tester.renderObject(find.byWidget(children[1], skipOffstage: false)).showOnScreen();
await tester.pumpAndSettle();
expect(controller.offset, 300.0);
});
testWidgets(
'Viewport showOnScreen should not scroll if the rect is already visible, even if it does not scroll linearly',
(WidgetTester tester) async {
List<Widget> children;
ScrollController controller;
const Key headerKey = Key('header');
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Center(
child: SizedBox(
height: 600.0,
child: CustomScrollView(
controller: controller = ScrollController(initialScrollOffset: 300.0),
slivers: children = List<Widget>.generate(20, (int i) {
return i == 10
? SliverPersistentHeader(
pinned: true,
delegate: _TestSliverPersistentHeaderDelegate(
minExtent: 100,
maxExtent: 300,
key: headerKey,
),
)
: SliverToBoxAdapter(
child: SizedBox(
height: 300.0,
child: Text('Tile $i'),
),
);
}),
),
),
),
),
);
controller.jumpTo(300.0 * 15);
await tester.pumpAndSettle();
final Finder pinnedHeaderContent = find.descendant(
of: find.byWidget(children[10]),
matching: find.byKey(headerKey),
);
// The persistent header is pinned to the leading edge thus still visible,
// the viewport should not scroll.
tester.renderObject(pinnedHeaderContent).showOnScreen();
await tester.pumpAndSettle();
expect(controller.offset, 300.0 * 15);
// The 11th child will be partially obstructed by the persistent header,
// the viewport should scroll to reveal it.
controller.jumpTo(
11 * 300.0 // Preceding headers
+ 200.0 // Shrinks the pinned header to minExtent
+ 100.0, // Obstructs the leading 100 pixels of the 11th header
);
await tester.pumpAndSettle();
tester.renderObject(find.byWidget(children[11], skipOffstage: false)).showOnScreen();
await tester.pumpAndSettle();
expect(controller.offset, lessThan(11 * 300.0 + 200.0 + 100.0));
},
);
void testFloatingHeaderShowOnScreen({ bool animated = true, Axis axis = Axis.vertical }) {
final TickerProvider? vsync = animated ? const TestVSync() : null;
const Key headerKey = Key('header');
late List<Widget> children;
final ScrollController controller = ScrollController(initialScrollOffset: 300.0);
Widget buildList({ required SliverPersistentHeader floatingHeader, bool reversed = false }) {
return Directionality(
textDirection: TextDirection.ltr,
child: Center(
child: SizedBox(
height: 400.0,
width: 400.0,
child: CustomScrollView(
scrollDirection: axis,
center: reversed ? const Key('19') : null,
controller: controller,
slivers: children = List<Widget>.generate(20, (int i) {
return i == 10
? floatingHeader
: SliverToBoxAdapter(
key: (i == 19) ? const Key('19') : null,
child: SizedBox(
height: 300.0,
width: 300,
child: Text('Tile $i'),
),
);
}),
),
),
),
);
}
double mainAxisExtent(WidgetTester tester, Finder finder) {
final RenderObject renderObject = tester.renderObject(finder);
if (renderObject is RenderSliver) {
return renderObject.geometry!.paintExtent;
}
final RenderBox renderBox = renderObject as RenderBox;
switch (axis) {
case Axis.horizontal:
return renderBox.size.width;
case Axis.vertical:
return renderBox.size.height;
}
}
group('animated: $animated, scrollDirection: $axis', () {
testWidgets(
'RenderViewportBase.showOnScreen',
(WidgetTester tester) async {
await tester.pumpWidget(
buildList(
floatingHeader: SliverPersistentHeader(
pinned: true,
floating: true,
delegate: _TestSliverPersistentHeaderDelegate(minExtent: 100, maxExtent: 300, key: headerKey, vsync: vsync),
),
),
);
final Finder pinnedHeaderContent = find.byKey(headerKey, skipOffstage: false);
controller.jumpTo(300.0 * 15);
await tester.pumpAndSettle();
expect(mainAxisExtent(tester, pinnedHeaderContent), lessThan(300));
// The persistent header is pinned to the leading edge thus still visible,
// the viewport should not scroll.
tester.renderObject(pinnedHeaderContent).showOnScreen(
descendant: tester.renderObject(pinnedHeaderContent),
rect: Offset.zero & const Size(300, 300),
);
await tester.pumpAndSettle();
// The header expands but doesn't move.
expect(controller.offset, 300.0 * 15);
expect(mainAxisExtent(tester, pinnedHeaderContent), 300);
// The rect specifies that the persistent header needs to be 1 pixel away
// from the leading edge of the viewport. Ignore the 1 pixel, the viewport
// should not scroll.
//
// See: https://github.com/flutter/flutter/issues/25507.
tester.renderObject(pinnedHeaderContent).showOnScreen(
descendant: tester.renderObject(pinnedHeaderContent),
rect: const Offset(-1, -1) & const Size(300, 300),
);
await tester.pumpAndSettle();
expect(controller.offset, 300.0 * 15);
expect(mainAxisExtent(tester, pinnedHeaderContent), 300);
},
);
testWidgets(
'RenderViewportBase.showOnScreen but no child',
(WidgetTester tester) async {
await tester.pumpWidget(
buildList(
floatingHeader: SliverPersistentHeader(
key: headerKey,
pinned: true,
floating: true,
delegate: _TestSliverPersistentHeaderDelegate(minExtent: 100, maxExtent: 300, vsync: vsync),
),
),
);
final Finder pinnedHeaderContent = find.byKey(headerKey, skipOffstage: false);
controller.jumpTo(300.0 * 15);
await tester.pumpAndSettle();
expect(mainAxisExtent(tester, pinnedHeaderContent), lessThan(300));
// The persistent header is pinned to the leading edge thus still visible,
// the viewport should not scroll.
tester.renderObject(pinnedHeaderContent).showOnScreen(
rect: Offset.zero & const Size(300, 300),
);
await tester.pumpAndSettle();
// The header expands but doesn't move.
expect(controller.offset, 300.0 * 15);
expect(mainAxisExtent(tester, pinnedHeaderContent), 300);
// The rect specifies that the persistent header needs to be 1 pixel away
// from the leading edge of the viewport. Ignore the 1 pixel, the viewport
// should not scroll.
//
// See: https://github.com/flutter/flutter/issues/25507.
tester.renderObject(pinnedHeaderContent).showOnScreen(
rect: const Offset(-1, -1) & const Size(300, 300),
);
await tester.pumpAndSettle();
expect(controller.offset, 300.0 * 15);
expect(mainAxisExtent(tester, pinnedHeaderContent), 300);
},
);
testWidgets(
'RenderViewportBase.showOnScreen with maxShowOnScreenExtent ',
(WidgetTester tester) async {
await tester.pumpWidget(
buildList(
floatingHeader: SliverPersistentHeader(
pinned: true,
floating: true,
delegate: _TestSliverPersistentHeaderDelegate(
minExtent: 100,
maxExtent: 300,
key: headerKey,
vsync: vsync,
showOnScreenConfiguration: const PersistentHeaderShowOnScreenConfiguration(maxShowOnScreenExtent: 200),
),
),
),
);
final Finder pinnedHeaderContent = find.byKey(headerKey, skipOffstage: false);
controller.jumpTo(300.0 * 15);
await tester.pumpAndSettle();
// childExtent was initially 100.
expect(mainAxisExtent(tester, pinnedHeaderContent), 100);
tester.renderObject(pinnedHeaderContent).showOnScreen(
descendant: tester.renderObject(pinnedHeaderContent),
rect: Offset.zero & const Size(300, 300),
);
await tester.pumpAndSettle();
// The header doesn't move. It would have expanded to 300 but
// maxShowOnScreenExtent is 200, preventing it from doing so.
expect(controller.offset, 300.0 * 15);
expect(mainAxisExtent(tester, pinnedHeaderContent), 200);
// ignoreLeading still works.
tester.renderObject(pinnedHeaderContent).showOnScreen(
descendant: tester.renderObject(pinnedHeaderContent),
rect: const Offset(-1, -1) & const Size(300, 300),
);
await tester.pumpAndSettle();
expect(controller.offset, 300.0 * 15);
expect(mainAxisExtent(tester, pinnedHeaderContent), 200);
// Move the viewport so that its childExtent reaches 250.
controller.jumpTo(300.0 * 10 + 50.0);
await tester.pumpAndSettle();
expect(mainAxisExtent(tester, pinnedHeaderContent), 250);
// Doesn't move, doesn't expand or shrink, leading still ignored.
tester.renderObject(pinnedHeaderContent).showOnScreen(
descendant: tester.renderObject(pinnedHeaderContent),
rect: const Offset(-1, -1) & const Size(300, 300),
);
await tester.pumpAndSettle();
expect(controller.offset, 300.0 * 10 + 50.0);
expect(mainAxisExtent(tester, pinnedHeaderContent), 250);
},
);
testWidgets(
'RenderViewportBase.showOnScreen with minShowOnScreenExtent ',
(WidgetTester tester) async {
await tester.pumpWidget(
buildList(
floatingHeader: SliverPersistentHeader(
pinned: true,
floating: true,
delegate: _TestSliverPersistentHeaderDelegate(
minExtent: 100,
maxExtent: 300,
key: headerKey,
vsync: vsync,
showOnScreenConfiguration: const PersistentHeaderShowOnScreenConfiguration(minShowOnScreenExtent: 200),
),
),
),
);
final Finder pinnedHeaderContent = find.byKey(headerKey, skipOffstage: false);
controller.jumpTo(300.0 * 15);
await tester.pumpAndSettle();
// childExtent was initially 100.
expect(mainAxisExtent(tester, pinnedHeaderContent), 100);
tester.renderObject(pinnedHeaderContent).showOnScreen(
descendant: tester.renderObject(pinnedHeaderContent),
rect: Offset.zero & const Size(110, 110),
);
await tester.pumpAndSettle();
// The header doesn't move. It would have expanded to 110 but
// minShowOnScreenExtent is 200, preventing it from doing so.
expect(controller.offset, 300.0 * 15);
expect(mainAxisExtent(tester, pinnedHeaderContent), 200);
// ignoreLeading still works.
tester.renderObject(pinnedHeaderContent).showOnScreen(
descendant: tester.renderObject(pinnedHeaderContent),
rect: const Offset(-1, -1) & const Size(110, 110),
);
await tester.pumpAndSettle();
expect(controller.offset, 300.0 * 15);
expect(mainAxisExtent(tester, pinnedHeaderContent), 200);
// Move the viewport so that its childExtent reaches 250.
controller.jumpTo(300.0 * 10 + 50.0);
await tester.pumpAndSettle();
expect(mainAxisExtent(tester, pinnedHeaderContent), 250);
// Doesn't move, doesn't expand or shrink, leading still ignored.
tester.renderObject(pinnedHeaderContent).showOnScreen(
descendant: tester.renderObject(pinnedHeaderContent),
rect: const Offset(-1, -1) & const Size(110, 110),
);
await tester.pumpAndSettle();
expect(controller.offset, 300.0 * 10 + 50.0);
expect(mainAxisExtent(tester, pinnedHeaderContent), 250);
},
);
testWidgets(
'RenderViewportBase.showOnScreen should not scroll if the rect is already visible, '
'even if it does not scroll linearly (reversed order version)',
(WidgetTester tester) async {
await tester.pumpWidget(
buildList(
floatingHeader: SliverPersistentHeader(
pinned: true,
floating: true,
delegate: _TestSliverPersistentHeaderDelegate(minExtent: 100, maxExtent: 300, key: headerKey, vsync: vsync),
),
reversed: true,
),
);
controller.jumpTo(-300.0 * 15);
await tester.pumpAndSettle();
final Finder pinnedHeaderContent = find.byKey(headerKey, skipOffstage: false);
// The persistent header is pinned to the leading edge thus still visible,
// the viewport should not scroll.
tester.renderObject(pinnedHeaderContent).showOnScreen();
await tester.pumpAndSettle();
expect(controller.offset, -300.0 * 15);
// children[9] will be partially obstructed by the persistent header,
// the viewport should scroll to reveal it.
controller.jumpTo(
- 8 * 300.0 // Preceding headers 11 - 18, children[11]'s top edge is aligned to the leading edge.
- 400.0 // Viewport height. children[10] (the pinned header) becomes pinned at the bottom of the screen.
- 200.0 // Shrinks the pinned header to minExtent (100).
- 100.0, // Obstructs the leading 100 pixels of the 11th header
);
await tester.pumpAndSettle();
tester.renderObject(find.byWidget(children[9], skipOffstage: false)).showOnScreen();
await tester.pumpAndSettle();
expect(controller.offset, -8 * 300.0 - 400.0 - 200.0);
},
);
});
}
group('Floating header showOnScreen', () {
testFloatingHeaderShowOnScreen();
testFloatingHeaderShowOnScreen(axis: Axis.horizontal);
});
group('RenderViewport getOffsetToReveal renderBox to sliver coordinates conversion', () {
const EdgeInsets padding = EdgeInsets.fromLTRB(22, 22, 34, 34);
const Key centerKey = Key('5');
Widget buildList({ required Axis axis, bool reverse = false, bool reverseGrowth = false }) {
return Directionality(
textDirection: TextDirection.ltr,
child: Center(
child: SizedBox(
height: 400.0,
width: 400.0,
child: CustomScrollView(
scrollDirection: axis,
reverse: reverse,
center: reverseGrowth ? centerKey : null,
slivers: List<Widget>.generate(6, (int i) {
return SliverPadding(
key: i == 5 ? centerKey : null,
padding: padding,
sliver: SliverToBoxAdapter(
child: Container(
padding: padding,
height: 300.0,
width: 300.0,
child: Text('Tile $i'),
),
),
);
}),
),
),
),
);
}
testWidgets('up, forward growth', (WidgetTester tester) async {
await tester.pumpWidget(buildList(axis: Axis.vertical, reverse: true));
final RenderAbstractViewport viewport = tester.allRenderObjects.whereType<RenderAbstractViewport>().first;
final RenderObject target = tester.renderObject(find.text('Tile 5', skipOffstage: false));
final double revealOffset = viewport.getOffsetToReveal(target, 0.0).offset;
expect(revealOffset, (300.0 + padding.horizontal) * 5 + 34.0 * 2);
});
testWidgets('up, reverse growth', (WidgetTester tester) async {
await tester.pumpWidget(buildList(axis: Axis.vertical, reverse: true, reverseGrowth: true));
final RenderAbstractViewport viewport = tester.allRenderObjects.whereType<RenderAbstractViewport>().first;
final RenderObject target = tester.renderObject(find.text('Tile 0', skipOffstage: false));
final double revealOffset = viewport.getOffsetToReveal(target, 0.0).offset;
expect(revealOffset, -(300.0 + padding.horizontal) * 5 + 34.0 * 2);
});
testWidgets('right, forward growth', (WidgetTester tester) async {
await tester.pumpWidget(buildList(axis: Axis.horizontal));
final RenderAbstractViewport viewport = tester.allRenderObjects.whereType<RenderAbstractViewport>().first;
final RenderObject target = tester.renderObject(find.text('Tile 5', skipOffstage: false));
final double revealOffset = viewport.getOffsetToReveal(target, 0.0).offset;
expect(revealOffset, (300.0 + padding.horizontal) * 5 + 22.0 * 2);
});
testWidgets('right, reverse growth', (WidgetTester tester) async {
await tester.pumpWidget(buildList(axis: Axis.horizontal, reverseGrowth: true));
final RenderAbstractViewport viewport = tester.allRenderObjects.whereType<RenderAbstractViewport>().first;
final RenderObject target = tester.renderObject(find.text('Tile 0', skipOffstage: false));
final double revealOffset = viewport.getOffsetToReveal(target, 0.0).offset;
expect(revealOffset, -(300.0 + padding.horizontal) * 5 + 22.0 * 2);
});
testWidgets('down, forward growth', (WidgetTester tester) async {
await tester.pumpWidget(buildList(axis: Axis.vertical));
final RenderAbstractViewport viewport = tester.allRenderObjects.whereType<RenderAbstractViewport>().first;
final RenderObject target = tester.renderObject(find.text('Tile 5', skipOffstage: false));
final double revealOffset = viewport.getOffsetToReveal(target, 0.0).offset;
expect(revealOffset, (300.0 + padding.horizontal) * 5 + 22.0 * 2);
});
testWidgets('down, reverse growth', (WidgetTester tester) async {
await tester.pumpWidget(buildList(axis: Axis.vertical, reverseGrowth: true));
final RenderAbstractViewport viewport = tester.allRenderObjects.whereType<RenderAbstractViewport>().first;
final RenderObject target = tester.renderObject(find.text('Tile 0', skipOffstage: false));
final double revealOffset = viewport.getOffsetToReveal(target, 0.0).offset;
expect(revealOffset, -(300.0 + padding.horizontal) * 5 + 22.0 * 2);
});
testWidgets('left, forward growth', (WidgetTester tester) async {
await tester.pumpWidget(buildList(axis: Axis.horizontal, reverse: true));
final RenderAbstractViewport viewport = tester.allRenderObjects.whereType<RenderAbstractViewport>().first;
final RenderObject target = tester.renderObject(find.text('Tile 5', skipOffstage: false));
final double revealOffset = viewport.getOffsetToReveal(target, 0.0).offset;
expect(revealOffset, (300.0 + padding.horizontal) * 5 + 34.0 * 2);
});
testWidgets('left, reverse growth', (WidgetTester tester) async {
await tester.pumpWidget(buildList(axis: Axis.horizontal, reverse: true, reverseGrowth: true));
final RenderAbstractViewport viewport = tester.allRenderObjects.whereType<RenderAbstractViewport>().first;
final RenderObject target = tester.renderObject(find.text('Tile 0', skipOffstage: false));
final double revealOffset = viewport.getOffsetToReveal(target, 0.0).offset;
expect(revealOffset, -(300.0 + padding.horizontal) * 5 + 34.0 * 2);
});
});
testWidgets('RenderViewportBase.showOnScreen reports the correct targetRect', (WidgetTester tester) async {
final ScrollController innerController = ScrollController();
final ScrollController outerController = ScrollController();
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Center(
child: SizedBox(
height: 300.0,
child: CustomScrollView(
cacheExtent: 0,
controller: outerController,
slivers: <Widget>[
SliverToBoxAdapter(
child: SizedBox(
height: 300,
child: CustomScrollView(
controller: innerController,
slivers: List<Widget>.generate(5, (int i) {
return SliverToBoxAdapter(
child: SizedBox(
height: 300.0,
child: Text('Tile $i'),
),
);
}),
),
),
),
const SliverToBoxAdapter(
child: SizedBox(
height: 300.0,
child: Text('hidden'),
),
),
],
),
),
),
),
);
tester.renderObject(find.widgetWithText(SizedBox, 'Tile 1', skipOffstage: false).first).showOnScreen();
await tester.pumpAndSettle();
// The inner viewport scrolls to reveal the 2nd tile.
expect(innerController.offset, 300.0);
expect(outerController.offset, 0);
});
group('unbounded constraints control test', () {
Widget buildNestedWidget([Axis a1 = Axis.vertical, Axis a2 = Axis.horizontal]) {
return Directionality(
textDirection: TextDirection.ltr,
child: Center(
child: ListView(
scrollDirection: a1,
children: List<Widget>.generate(10, (int y) {
return ListView(
scrollDirection: a2,
);
}),
),
),
);
}
Future<void> expectFlutterError({
required Widget widget,
required WidgetTester tester,
required String message,
}) async {
final List<FlutterErrorDetails> errors = <FlutterErrorDetails>[];
final FlutterExceptionHandler? oldHandler = FlutterError.onError;
FlutterError.onError = (FlutterErrorDetails error) => errors.add(error);
try {
await tester.pumpWidget(widget);
} finally {
FlutterError.onError = oldHandler;
}
expect(errors, isNotEmpty);
expect(errors.first.exception, isFlutterError);
expect((errors.first.exception as FlutterError).toStringDeep(), message);
}
testWidgets('Horizontal viewport was given unbounded height', (WidgetTester tester) async {
await expectFlutterError(
widget: buildNestedWidget(),
tester: tester,
message:
'FlutterError\n'
' Horizontal viewport was given unbounded height.\n'
' Viewports expand in the cross axis to fill their container and\n'
' constrain their children to match their extent in the cross axis.\n'
' In this case, a horizontal viewport was given an unlimited amount\n'
' of vertical space in which to expand.\n',
);
});
testWidgets('Horizontal viewport was given unbounded width', (WidgetTester tester) async {
await expectFlutterError(
widget: buildNestedWidget(Axis.horizontal),
tester: tester,
message:
'FlutterError\n'
' Horizontal viewport was given unbounded width.\n'
' Viewports expand in the scrolling direction to fill their\n'
' container. In this case, a horizontal viewport was given an\n'
' unlimited amount of horizontal space in which to expand. This\n'
' situation typically happens when a scrollable widget is nested\n'
' inside another scrollable widget.\n'
' If this widget is always nested in a scrollable widget there is\n'
' no need to use a viewport because there will always be enough\n'
' horizontal space for the children. In this case, consider using a\n'
' Row or Wrap instead. Otherwise, consider using a CustomScrollView\n'
' to concatenate arbitrary slivers into a single scrollable.\n',
);
});
testWidgets('Vertical viewport was given unbounded width', (WidgetTester tester) async {
await expectFlutterError(
widget: buildNestedWidget(Axis.horizontal, Axis.vertical),
tester: tester,
message:
'FlutterError\n'
' Vertical viewport was given unbounded width.\n'
' Viewports expand in the cross axis to fill their container and\n'
' constrain their children to match their extent in the cross axis.\n'
' In this case, a vertical viewport was given an unlimited amount\n'
' of horizontal space in which to expand.\n',
);
});
testWidgets('Vertical viewport was given unbounded height', (WidgetTester tester) async {
await expectFlutterError(
widget: buildNestedWidget(Axis.vertical, Axis.vertical),
tester: tester,
message:
'FlutterError\n'
' Vertical viewport was given unbounded height.\n'
' Viewports expand in the scrolling direction to fill their\n'
' container. In this case, a vertical viewport was given an\n'
' unlimited amount of vertical space in which to expand. This\n'
' situation typically happens when a scrollable widget is nested\n'
' inside another scrollable widget.\n'
' If this widget is always nested in a scrollable widget there is\n'
' no need to use a viewport because there will always be enough\n'
' vertical space for the children. In this case, consider using a\n'
' Column or Wrap instead. Otherwise, consider using a\n'
' CustomScrollView to concatenate arbitrary slivers into a single\n'
' scrollable.\n',
);
});
});
test('Viewport debugThrowIfNotCheckingIntrinsics() control test', () {
final RenderViewport renderViewport = RenderViewport(
crossAxisDirection: AxisDirection.right, offset: ViewportOffset.zero(),
);
late FlutterError error;
try {
renderViewport.computeMinIntrinsicHeight(0);
} on FlutterError catch (e) {
error = e;
}
expect(
error.toStringDeep(),
'FlutterError\n'
' RenderViewport does not support returning intrinsic dimensions.\n'
' Calculating the intrinsic dimensions would require instantiating\n'
' every child of the viewport, which defeats the point of viewports\n'
' being lazy.\n'
' If you are merely trying to shrink-wrap the viewport in the main\n'
' axis direction, consider a RenderShrinkWrappingViewport render\n'
' object (ShrinkWrappingViewport widget), which achieves that\n'
' effect without implementing the intrinsic dimension API.\n',
);
final RenderShrinkWrappingViewport renderShrinkWrappingViewport = RenderShrinkWrappingViewport(
crossAxisDirection: AxisDirection.right, offset: ViewportOffset.zero(),
);
try {
renderShrinkWrappingViewport.computeMinIntrinsicHeight(0);
} on FlutterError catch (e) {
error = e;
}
expect(error, isNotNull);
expect(
error.toStringDeep(),
'FlutterError\n'
' RenderShrinkWrappingViewport does not support returning intrinsic\n'
' dimensions.\n'
' Calculating the intrinsic dimensions would require instantiating\n'
' every child of the viewport, which defeats the point of viewports\n'
' being lazy.\n'
' If you are merely trying to shrink-wrap the viewport in the main\n'
' axis direction, you should be able to achieve that effect by just\n'
' giving the viewport loose constraints, without needing to measure\n'
' its intrinsic dimensions.\n',
);
});
group('Viewport childrenInPaintOrder control test', () {
test('RenderViewport', () async {
final List<RenderSliver> children = <RenderSliver>[
RenderSliverToBoxAdapter(),
RenderSliverToBoxAdapter(),
RenderSliverToBoxAdapter(),
];
final RenderViewport renderViewport = RenderViewport(
crossAxisDirection: AxisDirection.right,
offset: ViewportOffset.zero(),
children: children,
);
// Children should be painted in reverse order to the list given
expect(renderViewport.childrenInPaintOrder, equals(children.reversed));
// childrenInPaintOrder should be reverse of childrenInHitTestOrder
expect(
renderViewport.childrenInPaintOrder,
equals(renderViewport.childrenInHitTestOrder.toList().reversed),
);
});
test('RenderShrinkWrappingViewport', () async {
final List<RenderSliver> children = <RenderSliver>[
RenderSliverToBoxAdapter(),
RenderSliverToBoxAdapter(),
RenderSliverToBoxAdapter(),
];
final RenderShrinkWrappingViewport renderViewport =
RenderShrinkWrappingViewport(
crossAxisDirection: AxisDirection.right,
offset: ViewportOffset.zero(),
children: children,
);
// Children should be painted in reverse order to the list given
expect(renderViewport.childrenInPaintOrder, equals(children.reversed));
// childrenInPaintOrder should be reverse of childrenInHitTestOrder
expect(
renderViewport.childrenInPaintOrder,
equals(renderViewport.childrenInHitTestOrder.toList().reversed),
);
});
});
group('Overscrolling RenderShrinkWrappingViewport', () {
Widget buildSimpleShrinkWrap({
ScrollController? controller,
Axis scrollDirection = Axis.vertical,
ScrollPhysics? physics,
}) {
return Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: const MediaQueryData(),
child: ListView.builder(
controller: controller,
physics: physics,
scrollDirection: scrollDirection,
shrinkWrap: true,
itemBuilder: (BuildContext context, int index) => SizedBox(height: 50, width: 50, child: Text('Item $index')),
itemCount: 20,
itemExtent: 50,
),
),
);
}
Widget buildClippingShrinkWrap(
ScrollController controller, {
bool constrain = false,
}) {
return Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: const MediaQueryData(),
child: Container(
color: const Color(0xFF000000),
child: Column(
children: <Widget>[
// Translucent boxes above and below the shrinkwrapped viewport
// make it easily discernible if the viewport is not being
// clipped properly.
Opacity(
opacity: 0.5,
child: Container(height: 100, color: const Color(0xFF00B0FF)),
),
Container(
height: constrain ? 150 : null,
color: const Color(0xFFF44336),
child: ListView.builder(
controller: controller,
shrinkWrap: true,
physics: const BouncingScrollPhysics(parent: AlwaysScrollableScrollPhysics()),
itemBuilder: (BuildContext context, int index) => Text('Item $index'),
itemCount: 10,
),
),
Opacity(
opacity: 0.5,
child: Container(height: 100, color: const Color(0xFF00B0FF)),
),
],
),
),
),
);
}
testWidgets('constrained viewport correctly clips overflow', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/89717
final ScrollController controller = ScrollController();
await tester.pumpWidget(
buildClippingShrinkWrap(controller, constrain: true)
);
expect(controller.offset, 0.0);
expect(tester.getTopLeft(find.text('Item 0')).dy, 100.0);
expect(tester.getTopLeft(find.text('Item 9')).dy, 226.0);
// Overscroll
final TestGesture overscrollGesture = await tester.startGesture(tester.getCenter(find.text('Item 0')));
await overscrollGesture.moveBy(const Offset(0, 100));
await tester.pump();
expect(controller.offset, -100.0);
expect(tester.getTopLeft(find.text('Item 0')).dy, 200.0);
await expectLater(
find.byType(Directionality),
matchesGoldenFile('shrinkwrap_clipped_constrained_overscroll.png'),
);
await overscrollGesture.up();
await tester.pumpAndSettle();
expect(controller.offset, 0.0);
expect(tester.getTopLeft(find.text('Item 0')).dy, 100.0);
expect(tester.getTopLeft(find.text('Item 9')).dy, 226.0);
});
testWidgets('correctly clips overflow without constraints', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/89717
final ScrollController controller = ScrollController();
await tester.pumpWidget(
buildClippingShrinkWrap(controller)
);
expect(controller.offset, 0.0);
expect(tester.getTopLeft(find.text('Item 0')).dy, 100.0);
expect(tester.getTopLeft(find.text('Item 9')).dy, 226.0);
// Overscroll
final TestGesture overscrollGesture = await tester.startGesture(tester.getCenter(find.text('Item 0')));
await overscrollGesture.moveBy(const Offset(0, 100));
await tester.pump();
expect(controller.offset, -100.0);
expect(tester.getTopLeft(find.text('Item 0')).dy, 200.0);
await expectLater(
find.byType(Directionality),
matchesGoldenFile('shrinkwrap_clipped_overscroll.png'),
);
await overscrollGesture.up();
await tester.pumpAndSettle();
expect(controller.offset, 0.0);
expect(tester.getTopLeft(find.text('Item 0')).dy, 100.0);
expect(tester.getTopLeft(find.text('Item 9')).dy, 226.0);
});
testWidgets('allows overscrolling on default platforms - vertical', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/10949
// Scrollables should overscroll by default on iOS and macOS
final ScrollController controller = ScrollController();
await tester.pumpWidget(
buildSimpleShrinkWrap(controller: controller),
);
expect(controller.offset, 0.0);
expect(tester.getTopLeft(find.text('Item 0')).dy, 0.0);
// Check overscroll at both ends
// Start
TestGesture overscrollGesture = await tester.startGesture(tester.getCenter(find.byType(ListView)));
await overscrollGesture.moveBy(const Offset(0, 25));
await tester.pump();
expect(controller.offset, -25.0);
expect(tester.getTopLeft(find.text('Item 0')).dy, 25.0);
await overscrollGesture.up();
await tester.pumpAndSettle();
expect(controller.offset, 0.0);
expect(tester.getTopLeft(find.text('Item 0')).dy, 0.0);
// End
final double maxExtent = controller.position.maxScrollExtent;
controller.jumpTo(controller.position.maxScrollExtent);
await tester.pumpAndSettle();
expect(controller.offset, maxExtent);
expect(tester.getBottomLeft(find.text('Item 19')).dy, 600.0);
overscrollGesture = await tester.startGesture(tester.getCenter(find.byType(ListView)));
await overscrollGesture.moveBy(const Offset(0, -25));
await tester.pump();
expect(controller.offset, greaterThan(maxExtent));
expect(tester.getBottomLeft(find.text('Item 19')).dy, 575.0);
await overscrollGesture.up();
await tester.pumpAndSettle();
expect(controller.offset, maxExtent);
expect(tester.getBottomLeft(find.text('Item 19')).dy, 600.0);
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));
testWidgets('allows overscrolling on default platforms - horizontal', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/10949
// Scrollables should overscroll by default on iOS and macOS
final ScrollController controller = ScrollController();
await tester.pumpWidget(
buildSimpleShrinkWrap(controller: controller, scrollDirection: Axis.horizontal),
);
expect(controller.offset, 0.0);
expect(tester.getTopLeft(find.text('Item 0')).dx, 0.0);
// Check overscroll at both ends
// Start
TestGesture overscrollGesture = await tester.startGesture(tester.getCenter(find.byType(ListView)));
await overscrollGesture.moveBy(const Offset(25, 0));
await tester.pump();
expect(controller.offset, -25.0);
expect(tester.getTopLeft(find.text('Item 0')).dx, 25.0);
await overscrollGesture.up();
await tester.pumpAndSettle();
expect(controller.offset, 0.0);
expect(tester.getTopLeft(find.text('Item 0')).dx, 0.0);
// End
final double maxExtent = controller.position.maxScrollExtent;
controller.jumpTo(controller.position.maxScrollExtent);
await tester.pumpAndSettle();
expect(controller.offset, maxExtent);
expect(tester.getTopRight(find.text('Item 19')).dx, 800.0);
overscrollGesture = await tester.startGesture(tester.getCenter(find.byType(ListView)));
await overscrollGesture.moveBy(const Offset(-25, 0));
await tester.pump();
expect(controller.offset, greaterThan(maxExtent));
expect(tester.getTopRight(find.text('Item 19')).dx, 775.0);
await overscrollGesture.up();
await tester.pumpAndSettle();
expect(controller.offset, maxExtent);
expect(tester.getTopRight(find.text('Item 19')).dx, 800.0);
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));
testWidgets('allows overscrolling per physics - vertical', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/10949
// Scrollables should overscroll when the scroll physics allow
final ScrollController controller = ScrollController();
await tester.pumpWidget(
buildSimpleShrinkWrap(controller: controller, physics: const BouncingScrollPhysics()),
);
expect(controller.offset, 0.0);
expect(tester.getTopLeft(find.text('Item 0')).dy, 0.0);
// Check overscroll at both ends
// Start
TestGesture overscrollGesture = await tester.startGesture(tester.getCenter(find.byType(ListView)));
await overscrollGesture.moveBy(const Offset(0, 25));
await tester.pump();
expect(controller.offset, -25.0);
expect(tester.getTopLeft(find.text('Item 0')).dy, 25.0);
await overscrollGesture.up();
await tester.pumpAndSettle();
expect(controller.offset, 0.0);
expect(tester.getTopLeft(find.text('Item 0')).dy, 0.0);
// End
final double maxExtent = controller.position.maxScrollExtent;
controller.jumpTo(controller.position.maxScrollExtent);
await tester.pumpAndSettle();
expect(controller.offset, maxExtent);
expect(tester.getBottomLeft(find.text('Item 19')).dy, 600.0);
overscrollGesture = await tester.startGesture(tester.getCenter(find.byType(ListView)));
await overscrollGesture.moveBy(const Offset(0, -25));
await tester.pump();
expect(controller.offset, greaterThan(maxExtent));
expect(tester.getBottomLeft(find.text('Item 19')).dy, 575.0);
await overscrollGesture.up();
await tester.pumpAndSettle();
expect(controller.offset, maxExtent);
expect(tester.getBottomLeft(find.text('Item 19')).dy, 600.0);
});
testWidgets('allows overscrolling per physics - horizontal', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/10949
// Scrollables should overscroll when the scroll physics allow
final ScrollController controller = ScrollController();
await tester.pumpWidget(
buildSimpleShrinkWrap(
controller: controller,
scrollDirection: Axis.horizontal,
physics: const BouncingScrollPhysics(),
),
);
expect(controller.offset, 0.0);
expect(tester.getTopLeft(find.text('Item 0')).dx, 0.0);
// Check overscroll at both ends
// Start
TestGesture overscrollGesture = await tester.startGesture(tester.getCenter(find.byType(ListView)));
await overscrollGesture.moveBy(const Offset(25, 0));
await tester.pump();
expect(controller.offset, -25.0);
expect(tester.getTopLeft(find.text('Item 0')).dx, 25.0);
await overscrollGesture.up();
await tester.pumpAndSettle();
expect(controller.offset, 0.0);
expect(tester.getTopLeft(find.text('Item 0')).dx, 0.0);
// End
final double maxExtent = controller.position.maxScrollExtent;
controller.jumpTo(controller.position.maxScrollExtent);
await tester.pumpAndSettle();
expect(controller.offset, maxExtent);
expect(tester.getTopRight(find.text('Item 19')).dx, 800.0);
overscrollGesture = await tester.startGesture(tester.getCenter(find.byType(ListView)));
await overscrollGesture.moveBy(const Offset(-25, 0));
await tester.pump();
expect(controller.offset, greaterThan(maxExtent));
expect(tester.getTopRight(find.text('Item 19')).dx, 775.0);
await overscrollGesture.up();
await tester.pumpAndSettle();
expect(controller.offset, maxExtent);
expect(tester.getTopRight(find.text('Item 19')).dx, 800.0);
});
});
testWidgets('Handles infinite constraints when TargetPlatform is iOS or macOS', (WidgetTester tester) async {
// regression test for https://github.com/flutter/flutter/issues/45866
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: const MediaQueryData(),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
GridView(
shrinkWrap: true,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
childAspectRatio: 3,
mainAxisSpacing: 3,
crossAxisSpacing: 3,
),
children: const <Widget>[
Text('a'),
Text('b'),
Text('c'),
],
),
],
),
),
),
);
expect(find.text('b'), findsOneWidget);
await tester.drag(find.text('b'), const Offset(0, 200));
await tester.pumpAndSettle();
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));
testWidgets('Viewport describeApproximateClip respects clipBehavior', (WidgetTester tester) async {
await tester.pumpWidget(const Directionality(
textDirection: TextDirection.ltr,
child: CustomScrollView(
clipBehavior: Clip.none,
slivers: <Widget>[
SliverToBoxAdapter(child: SizedBox(width: 20, height: 20)),
]
),
));
RenderViewport viewport = tester.allRenderObjects.whereType<RenderViewport>().first;
expect(viewport.clipBehavior, Clip.none);
bool visited = false;
viewport.visitChildren((RenderObject child) {
visited = true;
expect(viewport.describeApproximatePaintClip(child as RenderSliver), null);
});
expect(visited, true);
await tester.pumpWidget(const Directionality(
textDirection: TextDirection.ltr,
child: CustomScrollView(
slivers: <Widget>[
SliverToBoxAdapter(child: SizedBox(width: 20, height: 20)),
]
),
));
viewport = tester.allRenderObjects.whereType<RenderViewport>().first;
expect(viewport.clipBehavior, Clip.hardEdge);
visited = false;
viewport.visitChildren((RenderObject child) {
visited = true;
expect(viewport.describeApproximatePaintClip(child as RenderSliver), Offset.zero & viewport.size);
});
expect(visited, true);
});
}