blob: f92c2fab916bbc92605eac640354a0d08278a7d3 [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.
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
import '../rendering/rendering_tester.dart';
import 'semantics_tester.dart';
class TestScrollPosition extends ScrollPositionWithSingleContext {
TestScrollPosition({
required ScrollPhysics physics,
required ScrollContext state,
double initialPixels = 0.0,
ScrollPosition? oldPosition,
}) : super(
physics: physics,
context: state,
initialPixels: initialPixels,
oldPosition: oldPosition,
);
}
class TestScrollController extends ScrollController {
@override
ScrollPosition createScrollPosition(ScrollPhysics physics, ScrollContext context, ScrollPosition? oldPosition) {
return TestScrollPosition(
physics: physics,
state: context,
initialPixels: initialScrollOffset,
oldPosition: oldPosition,
);
}
}
void main() {
testWidgets('SingleChildScrollView overflow and clipRect test', (WidgetTester tester) async {
// the test widowSize is Size(800.0, 600.0)
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: SingleChildScrollView(
child: Container(height: 600.0),
),
),
);
// 1st, check that the render object has received the default clip behavior.
final dynamic renderObject = tester.allRenderObjects.where((RenderObject o) => o.runtimeType.toString() == '_RenderSingleChildViewport').first;
expect(renderObject.clipBehavior, equals(Clip.hardEdge)); // ignore: avoid_dynamic_calls
// 2nd, height == widow.height test: check that the painting context does not call pushClipRect .
TestClipPaintingContext context = TestClipPaintingContext();
renderObject.paint(context, Offset.zero); // ignore: avoid_dynamic_calls
expect(context.clipBehavior, equals(Clip.none));
// 3rd, height overflow test: check that the painting context call pushClipRect.
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: SingleChildScrollView(
child: Container(height: 600.1),
),
),
);
renderObject.paint(context, Offset.zero); // ignore: avoid_dynamic_calls
expect(context.clipBehavior, equals(Clip.hardEdge));
// 4th, width == widow.width test: check that the painting context do not call pushClipRect.
context = TestClipPaintingContext();
expect(context.clipBehavior, equals(Clip.none)); // initial value
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Container(width: 800.0),
),
),
);
renderObject.paint(context, Offset.zero); // ignore: avoid_dynamic_calls
expect(context.clipBehavior, equals(Clip.none));
// 5th, width overflow test: check that the painting context call pushClipRect.
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Container(width: 800.1),
),
),
);
renderObject.paint(context, Offset.zero); // ignore: avoid_dynamic_calls
expect(context.clipBehavior, equals(Clip.hardEdge));
});
testWidgets('SingleChildScrollView respects clipBehavior', (WidgetTester tester) async {
await tester.pumpWidget(SingleChildScrollView(child: Container(height: 2000.0)));
// 1st, check that the render object has received the default clip behavior.
final dynamic renderObject = tester.allRenderObjects.where((RenderObject o) => o.runtimeType.toString() == '_RenderSingleChildViewport').first;
expect(renderObject.clipBehavior, equals(Clip.hardEdge)); // ignore: avoid_dynamic_calls
// 2nd, check that the painting context has received the default clip behavior.
final TestClipPaintingContext context = TestClipPaintingContext();
renderObject.paint(context, Offset.zero); // ignore: avoid_dynamic_calls
expect(context.clipBehavior, equals(Clip.hardEdge));
// 3rd, pump a new widget to check that the render object can update its clip behavior.
await tester.pumpWidget(SingleChildScrollView(clipBehavior: Clip.antiAlias, child: Container(height: 2000.0)));
expect(renderObject.clipBehavior, equals(Clip.antiAlias)); // ignore: avoid_dynamic_calls
// 4th, check that a non-default clip behavior can be sent to the painting context.
renderObject.paint(context, Offset.zero); // ignore: avoid_dynamic_calls
expect(context.clipBehavior, equals(Clip.antiAlias));
});
testWidgets('SingleChildScrollView control test', (WidgetTester tester) async {
await tester.pumpWidget(SingleChildScrollView(
child: Container(
height: 2000.0,
color: const Color(0xFF00FF00),
),
));
final RenderBox box = tester.renderObject(find.byType(Container));
expect(box.localToGlobal(Offset.zero), equals(Offset.zero));
await tester.drag(find.byType(SingleChildScrollView), const Offset(-200.0, -200.0));
expect(box.localToGlobal(Offset.zero), equals(const Offset(0.0, -200.0)));
});
testWidgets('Changing controllers changes scroll position', (WidgetTester tester) async {
final TestScrollController controller = TestScrollController();
await tester.pumpWidget(SingleChildScrollView(
child: Container(
height: 2000.0,
color: const Color(0xFF00FF00),
),
));
await tester.pumpWidget(SingleChildScrollView(
controller: controller,
child: Container(
height: 2000.0,
color: const Color(0xFF00FF00),
),
));
final ScrollableState scrollable = tester.state(find.byType(Scrollable));
expect(scrollable.position, isA<TestScrollPosition>());
});
testWidgets('Sets PrimaryScrollController when primary', (WidgetTester tester) async {
final ScrollController primaryScrollController = ScrollController();
await tester.pumpWidget(PrimaryScrollController(
controller: primaryScrollController,
child: SingleChildScrollView(
primary: true,
child: Container(
height: 2000.0,
color: const Color(0xFF00FF00),
),
),
));
final Scrollable scrollable = tester.widget(find.byType(Scrollable));
expect(scrollable.controller, primaryScrollController);
});
testWidgets('Changing scroll controller inside dirty layout builder does not assert', (WidgetTester tester) async {
final ScrollController controller = ScrollController();
await tester.pumpWidget(Center(
child: SizedBox(
width: 750.0,
child: LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
return SingleChildScrollView(
child: Container(
height: 2000.0,
color: const Color(0xFF00FF00),
),
);
},
),
),
));
await tester.pumpWidget(Center(
child: SizedBox(
width: 700.0,
child: LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
return SingleChildScrollView(
controller: controller,
child: Container(
height: 2000.0,
color: const Color(0xFF00FF00),
),
);
},
),
),
));
});
testWidgets('Vertical SingleChildScrollViews are primary by default', (WidgetTester tester) async {
const SingleChildScrollView view = SingleChildScrollView();
expect(view.primary, isTrue);
});
testWidgets('Horizontal SingleChildScrollViews are non-primary by default', (WidgetTester tester) async {
const SingleChildScrollView view = SingleChildScrollView(scrollDirection: Axis.horizontal);
expect(view.primary, isFalse);
});
testWidgets('SingleChildScrollViews with controllers are non-primary by default', (WidgetTester tester) async {
final SingleChildScrollView view = SingleChildScrollView(
controller: ScrollController(),
);
expect(view.primary, isFalse);
});
testWidgets('Nested scrollables have a null PrimaryScrollController', (WidgetTester tester) async {
const Key innerKey = Key('inner');
final ScrollController primaryScrollController = ScrollController();
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: PrimaryScrollController(
controller: primaryScrollController,
child: SingleChildScrollView(
primary: true,
child: Container(
constraints: const BoxConstraints(maxHeight: 200.0),
child: ListView(key: innerKey, primary: true),
),
),
),
),
);
final Scrollable innerScrollable = tester.widget(
find.descendant(
of: find.byKey(innerKey),
matching: find.byType(Scrollable),
),
);
expect(innerScrollable.controller, isNull);
});
testWidgets('SingleChildScrollView semantics', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
final ScrollController controller = ScrollController();
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: SingleChildScrollView(
controller: controller,
child: Column(
children: List<Widget>.generate(30, (int i) {
return SizedBox(
height: 200.0,
child: Text('Tile $i'),
);
}),
),
),
),
);
expect(semantics, hasSemantics(
TestSemantics(
children: <TestSemantics>[
TestSemantics(
flags: <SemanticsFlag>[
SemanticsFlag.hasImplicitScrolling,
],
actions: <SemanticsAction>[
SemanticsAction.scrollUp,
],
children: <TestSemantics>[
TestSemantics(
label: r'Tile 0',
textDirection: TextDirection.ltr,
),
TestSemantics(
label: r'Tile 1',
textDirection: TextDirection.ltr,
),
TestSemantics(
label: r'Tile 2',
textDirection: TextDirection.ltr,
),
TestSemantics(
flags: <SemanticsFlag>[
SemanticsFlag.isHidden,
],
label: r'Tile 3',
textDirection: TextDirection.ltr,
),
TestSemantics(
flags: <SemanticsFlag>[
SemanticsFlag.isHidden,],
label: r'Tile 4',
textDirection: TextDirection.ltr,
),
],
),
],
),
ignoreRect: true, ignoreTransform: true, ignoreId: true,
));
controller.jumpTo(3000.0);
await tester.pumpAndSettle();
expect(semantics, hasSemantics(
TestSemantics(
children: <TestSemantics>[
TestSemantics(
flags: <SemanticsFlag>[
SemanticsFlag.hasImplicitScrolling,
],
actions: <SemanticsAction>[
SemanticsAction.scrollUp,
SemanticsAction.scrollDown,
],
children: <TestSemantics>[
TestSemantics(
flags: <SemanticsFlag>[
SemanticsFlag.isHidden,
],
label: r'Tile 13',
textDirection: TextDirection.ltr,
),
TestSemantics(
flags: <SemanticsFlag>[
SemanticsFlag.isHidden,
],
label: r'Tile 14',
textDirection: TextDirection.ltr,
),
TestSemantics(
label: r'Tile 15',
textDirection: TextDirection.ltr,
),
TestSemantics(
label: r'Tile 16',
textDirection: TextDirection.ltr,
),
TestSemantics(
label: r'Tile 17',
textDirection: TextDirection.ltr,
),
TestSemantics(
flags: <SemanticsFlag>[
SemanticsFlag.isHidden,
],
label: r'Tile 18',
textDirection: TextDirection.ltr,
),
TestSemantics(
flags: <SemanticsFlag>[
SemanticsFlag.isHidden,
],
label: r'Tile 19',
textDirection: TextDirection.ltr,
),
],
),
],
),
ignoreRect: true, ignoreTransform: true, ignoreId: true,
));
controller.jumpTo(6000.0);
await tester.pumpAndSettle();
expect(semantics, hasSemantics(
TestSemantics(
children: <TestSemantics>[
TestSemantics(
flags: <SemanticsFlag>[
SemanticsFlag.hasImplicitScrolling,
],
actions: <SemanticsAction>[
SemanticsAction.scrollDown,
],
children: <TestSemantics>[
TestSemantics(
flags: <SemanticsFlag>[
SemanticsFlag.isHidden,
],
label: r'Tile 25',
textDirection: TextDirection.ltr,
),
TestSemantics(
flags: <SemanticsFlag>[
SemanticsFlag.isHidden,
],
label: r'Tile 26',
textDirection: TextDirection.ltr,
),
TestSemantics(
label: r'Tile 27',
textDirection: TextDirection.ltr,
),
TestSemantics(
label: r'Tile 28',
textDirection: TextDirection.ltr,
),
TestSemantics(
label: r'Tile 29',
textDirection: TextDirection.ltr,
),
],
),
],
),
ignoreRect: true, ignoreTransform: true, ignoreId: true,
));
semantics.dispose();
});
testWidgets('SingleChildScrollView 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: SingleChildScrollView(
controller: ScrollController(initialScrollOffset: 300.0),
child: Column(
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]));
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('SingleChildScrollView getOffsetToReveal - up', (WidgetTester tester) async {
final List<Widget> children = List<Widget>.generate(20, (int i) {
return SizedBox(
height: 100.0,
width: 300.0,
child: Text('Tile $i'),
);
});
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Center(
child: SizedBox(
height: 200.0,
width: 300.0,
child: SingleChildScrollView(
controller: ScrollController(initialScrollOffset: 300.0),
reverse: true,
child: Column(
children: children.reversed.toList(),
),
),
),
),
),
);
final RenderAbstractViewport viewport = tester.allRenderObjects.whereType<RenderAbstractViewport>().first;
final RenderObject target = tester.renderObject(find.byWidget(children[5]));
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('SingleChildScrollView 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: SingleChildScrollView(
scrollDirection: Axis.horizontal,
controller: ScrollController(initialScrollOffset: 300.0),
child: Row(
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]));
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('SingleChildScrollView getOffsetToReveal - left', (WidgetTester tester) async {
final List<Widget> children = List<Widget>.generate(20, (int i) {
return SizedBox(
height: 300.0,
width: 100.0,
child: Text('Tile $i'),
);
});
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Center(
child: SizedBox(
height: 300.0,
width: 200.0,
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
reverse: true,
controller: ScrollController(initialScrollOffset: 300.0),
child: Row(
children: children.reversed.toList(),
),
),
),
),
),
);
final RenderAbstractViewport viewport = tester.allRenderObjects.whereType<RenderAbstractViewport>().first;
final RenderObject target = tester.renderObject(find.byWidget(children[5]));
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('Nested SingleChildScrollView showOnScreen', (WidgetTester tester) async {
final List<List<Widget>> children = List<List<Widget>>.generate(10, (int x) {
return List<Widget>.generate(10, (int y) {
return SizedBox(
key: UniqueKey(),
height: 100.0,
width: 100.0,
);
});
});
ScrollController controllerX;
ScrollController controllerY;
/// Builds a gird:
///
/// <- 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: SingleChildScrollView(
controller: controllerY = ScrollController(initialScrollOffset: 400.0),
child: SingleChildScrollView(
controller: controllerX = ScrollController(initialScrollOffset: 400.0),
scrollDirection: Axis.horizontal,
child: Column(
children: children.map((List<Widget> widgets) {
return Row(
children: widgets,
);
}).toList(),
),
),
),
),
),
),
);
expect(controllerX.offset, 400.0);
expect(controllerY.offset, 400.0);
// Already in viewport
tester.renderObject(find.byWidget(children[4][4])).showOnScreen();
await tester.pumpAndSettle();
expect(controllerX.offset, 400.0);
expect(controllerY.offset, 400.0);
controllerX.jumpTo(400.0);
controllerY.jumpTo(400.0);
await tester.pumpAndSettle();
// Above viewport
tester.renderObject(find.byWidget(children[3][4])).showOnScreen();
await tester.pumpAndSettle();
expect(controllerX.offset, 400.0);
expect(controllerY.offset, 300.0);
controllerX.jumpTo(400.0);
controllerY.jumpTo(400.0);
await tester.pumpAndSettle();
// Below viewport
tester.renderObject(find.byWidget(children[6][4])).showOnScreen();
await tester.pumpAndSettle();
expect(controllerX.offset, 400.0);
expect(controllerY.offset, 500.0);
controllerX.jumpTo(400.0);
controllerY.jumpTo(400.0);
await tester.pumpAndSettle();
// Left of viewport
tester.renderObject(find.byWidget(children[4][3])).showOnScreen();
await tester.pumpAndSettle();
expect(controllerX.offset, 300.0);
expect(controllerY.offset, 400.0);
controllerX.jumpTo(400.0);
controllerY.jumpTo(400.0);
await tester.pumpAndSettle();
// Right of viewport
tester.renderObject(find.byWidget(children[4][6])).showOnScreen();
await tester.pumpAndSettle();
expect(controllerX.offset, 500.0);
expect(controllerY.offset, 400.0);
controllerX.jumpTo(400.0);
controllerY.jumpTo(400.0);
await tester.pumpAndSettle();
// Above and left of viewport
tester.renderObject(find.byWidget(children[3][3])).showOnScreen();
await tester.pumpAndSettle();
expect(controllerX.offset, 300.0);
expect(controllerY.offset, 300.0);
controllerX.jumpTo(400.0);
controllerY.jumpTo(400.0);
await tester.pumpAndSettle();
// Below and left of viewport
tester.renderObject(find.byWidget(children[6][3])).showOnScreen();
await tester.pumpAndSettle();
expect(controllerX.offset, 300.0);
expect(controllerY.offset, 500.0);
controllerX.jumpTo(400.0);
controllerY.jumpTo(400.0);
await tester.pumpAndSettle();
// Above and right of viewport
tester.renderObject(find.byWidget(children[3][6])).showOnScreen();
await tester.pumpAndSettle();
expect(controllerX.offset, 500.0);
expect(controllerY.offset, 300.0);
controllerX.jumpTo(400.0);
controllerY.jumpTo(400.0);
await tester.pumpAndSettle();
// Below and right of viewport
tester.renderObject(find.byWidget(children[6][6])).showOnScreen();
await tester.pumpAndSettle();
expect(controllerX.offset, 500.0);
expect(controllerY.offset, 500.0);
controllerX.jumpTo(400.0);
controllerY.jumpTo(400.0);
await tester.pumpAndSettle();
// Below and right of viewport with animations
tester.renderObject(find.byWidget(children[6][6])).showOnScreen(duration: const Duration(seconds: 2));
await tester.pump();
await tester.pump(const Duration(seconds: 1));
expect(tester.hasRunningAnimations, isTrue);
expect(controllerX.offset, greaterThan(400.0));
expect(controllerX.offset, lessThan(500.0));
expect(controllerY.offset, greaterThan(400.0));
expect(controllerY.offset, lessThan(500.0));
await tester.pumpAndSettle();
expect(controllerX.offset, 500.0);
expect(controllerY.offset, 500.0);
});
group('Nested SingleChildScrollView (same orientation) showOnScreen', () {
late List<Widget> children;
Future<void> buildNestedScroller({ required WidgetTester tester, ScrollController? inner, ScrollController? outer }) {
return tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Center(
child: SizedBox(
height: 200.0,
width: 300.0,
child: SingleChildScrollView(
controller: outer,
child: Column(
children: <Widget>[
const SizedBox(
height: 200.0,
),
SizedBox(
height: 200.0,
width: 300.0,
child: SingleChildScrollView(
controller: inner,
child: Column(
children: children = List<Widget>.generate(10, (int i) {
return SizedBox(
height: 100.0,
width: 300.0,
child: Text('$i'),
);
}),
),
),
),
const SizedBox(
height: 200.0,
),
],
),
),
),
),
),
);
}
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])).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[5])).showOnScreen();
await tester.pumpAndSettle();
expect(inner.offset, 400.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])).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('keyboardDismissBehavior tests', (WidgetTester tester) async {
final List<FocusNode> focusNodes = List<FocusNode>.generate(50, (int i) => FocusNode());
Future<void> boilerplate(ScrollViewKeyboardDismissBehavior behavior) {
return tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: SingleChildScrollView(
padding: EdgeInsets.zero,
keyboardDismissBehavior: behavior,
child: Column(
children: focusNodes.map((FocusNode focusNode) {
return SizedBox(
height: 50,
child: TextField(focusNode: focusNode),
);
}).toList(),
),
),
),
),
);
}
// ScrollViewKeyboardDismissBehavior.onDrag dismiss keyboard on drag
await boilerplate(ScrollViewKeyboardDismissBehavior.onDrag);
Finder finder = find.byType(TextField).first;
TextField textField = tester.widget(finder);
await tester.showKeyboard(finder);
expect(textField.focusNode!.hasFocus, isTrue);
await tester.drag(finder, const Offset(0.0, -40.0));
await tester.pumpAndSettle();
expect(textField.focusNode!.hasFocus, isFalse);
// ScrollViewKeyboardDismissBehavior.manual does no dismiss the keyboard
await boilerplate(ScrollViewKeyboardDismissBehavior.manual);
finder = find.byType(TextField).first;
textField = tester.widget(finder);
await tester.showKeyboard(finder);
expect(textField.focusNode!.hasFocus, isTrue);
await tester.drag(finder, const Offset(0.0, -40.0));
await tester.pumpAndSettle();
expect(textField.focusNode!.hasFocus, isTrue);
});
}