blob: 1131e4b43b0a42967f5064ebb9e9f9893bd0d735 [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 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('SliverList reverse children (with keys)', (WidgetTester tester) async {
final List<int> items = List<int>.generate(20, (int i) => i);
const double itemHeight = 300.0;
const double viewportHeight = 500.0;
const double scrollPosition = 18 * itemHeight;
final ScrollController controller = ScrollController(initialScrollOffset: scrollPosition);
await tester.pumpWidget(_buildSliverList(
items: items,
controller: controller,
itemHeight: itemHeight,
viewportHeight: viewportHeight,
));
await tester.pumpAndSettle();
expect(controller.offset, scrollPosition);
expect(find.text('Tile 0'), findsNothing);
expect(find.text('Tile 1'), findsNothing);
expect(find.text('Tile 18'), findsOneWidget);
expect(find.text('Tile 19'), findsOneWidget);
await tester.pumpWidget(_buildSliverList(
items: items.reversed.toList(),
controller: controller,
itemHeight: itemHeight,
viewportHeight: viewportHeight,
));
final int frames = await tester.pumpAndSettle();
expect(frames, 1); // ensures that there is no (animated) bouncing of the scrollable
expect(controller.offset, scrollPosition);
expect(find.text('Tile 19'), findsNothing);
expect(find.text('Tile 18'), findsNothing);
expect(find.text('Tile 1'), findsOneWidget);
expect(find.text('Tile 0'), findsOneWidget);
controller.jumpTo(0.0);
await tester.pumpAndSettle();
expect(controller.offset, 0.0);
expect(find.text('Tile 19'), findsOneWidget);
expect(find.text('Tile 18'), findsOneWidget);
expect(find.text('Tile 1'), findsNothing);
expect(find.text('Tile 0'), findsNothing);
});
testWidgets('SliverList replace children (with keys)', (WidgetTester tester) async {
final List<int> items = List<int>.generate(20, (int i) => i);
const double itemHeight = 300.0;
const double viewportHeight = 500.0;
const double scrollPosition = 18 * itemHeight;
final ScrollController controller = ScrollController(initialScrollOffset: scrollPosition);
await tester.pumpWidget(_buildSliverList(
items: items,
controller: controller,
itemHeight: itemHeight,
viewportHeight: viewportHeight,
));
await tester.pumpAndSettle();
expect(controller.offset, scrollPosition);
expect(find.text('Tile 0'), findsNothing);
expect(find.text('Tile 1'), findsNothing);
expect(find.text('Tile 18'), findsOneWidget);
expect(find.text('Tile 19'), findsOneWidget);
await tester.pumpWidget(_buildSliverList(
items: items.map<int>((int i) => i + 100).toList(),
controller: controller,
itemHeight: itemHeight,
viewportHeight: viewportHeight,
));
final int frames = await tester.pumpAndSettle();
expect(frames, 1); // ensures that there is no (animated) bouncing of the scrollable
expect(controller.offset, scrollPosition);
expect(find.text('Tile 0'), findsNothing);
expect(find.text('Tile 1'), findsNothing);
expect(find.text('Tile 18'), findsNothing);
expect(find.text('Tile 19'), findsNothing);
expect(find.text('Tile 100'), findsNothing);
expect(find.text('Tile 101'), findsNothing);
expect(find.text('Tile 118'), findsOneWidget);
expect(find.text('Tile 119'), findsOneWidget);
controller.jumpTo(0.0);
await tester.pumpAndSettle();
expect(controller.offset, 0.0);
expect(find.text('Tile 100'), findsOneWidget);
expect(find.text('Tile 101'), findsOneWidget);
expect(find.text('Tile 118'), findsNothing);
expect(find.text('Tile 119'), findsNothing);
});
testWidgets('SliverList replace with shorter children list (with keys)', (WidgetTester tester) async {
final List<int> items = List<int>.generate(20, (int i) => i);
const double itemHeight = 300.0;
const double viewportHeight = 500.0;
final double scrollPosition = items.length * itemHeight - viewportHeight;
final ScrollController controller = ScrollController(initialScrollOffset: scrollPosition);
await tester.pumpWidget(_buildSliverList(
items: items,
controller: controller,
itemHeight: itemHeight,
viewportHeight: viewportHeight,
));
await tester.pumpAndSettle();
expect(controller.offset, scrollPosition);
expect(find.text('Tile 0'), findsNothing);
expect(find.text('Tile 1'), findsNothing);
expect(find.text('Tile 17'), findsNothing);
expect(find.text('Tile 18'), findsOneWidget);
expect(find.text('Tile 19'), findsOneWidget);
await tester.pumpWidget(_buildSliverList(
items: items.sublist(0, items.length - 1),
controller: controller,
itemHeight: itemHeight,
viewportHeight: viewportHeight,
));
final int frames = await tester.pumpAndSettle();
expect(frames, 1); // No animation when content shrinks suddenly.
expect(controller.offset, scrollPosition - itemHeight);
expect(find.text('Tile 0'), findsNothing);
expect(find.text('Tile 1'), findsNothing);
expect(find.text('Tile 17'), findsOneWidget);
expect(find.text('Tile 18'), findsOneWidget);
expect(find.text('Tile 19'), findsNothing);
});
testWidgets('SliverList should layout first child in case of child reordering', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/35904.
List<String> items = <String>['1', '2'];
await tester.pumpWidget(_buildSliverListRenderWidgetChild(items));
await tester.pumpAndSettle();
expect(find.text('Tile 1'), findsOneWidget);
expect(find.text('Tile 2'), findsOneWidget);
items = items.reversed.toList();
await tester.pumpWidget(_buildSliverListRenderWidgetChild(items));
await tester.pumpAndSettle();
expect(find.text('Tile 1'), findsOneWidget);
expect(find.text('Tile 2'), findsOneWidget);
});
testWidgets('SliverList should recalculate inaccurate layout offset case 1', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/42142.
final List<int> items = List<int>.generate(20, (int i) => i);
final ScrollController controller = ScrollController();
await tester.pumpWidget(
_buildSliverList(
items: List<int>.from(items),
controller: controller,
itemHeight: 50,
viewportHeight: 200,
),
);
await tester.pumpAndSettle();
await tester.drag(find.text('Tile 2'), const Offset(0.0, -1000.0));
await tester.pumpAndSettle();
// Viewport should be scrolled to the end of list.
expect(controller.offset, 800.0);
expect(find.text('Tile 15'), findsNothing);
expect(find.text('Tile 16'), findsOneWidget);
expect(find.text('Tile 17'), findsOneWidget);
expect(find.text('Tile 18'), findsOneWidget);
expect(find.text('Tile 19'), findsOneWidget);
// Prepends item to the list.
items.insert(0, -1);
await tester.pumpWidget(
_buildSliverList(
items: List<int>.from(items),
controller: controller,
itemHeight: 50,
viewportHeight: 200,
),
);
await tester.pump();
// We need second pump to ensure the scheduled animation gets run.
await tester.pumpAndSettle();
// Scroll offset should stay the same, and the items in viewport should be
// shifted by one.
expect(controller.offset, 800.0);
expect(find.text('Tile 14'), findsNothing);
expect(find.text('Tile 15'), findsOneWidget);
expect(find.text('Tile 16'), findsOneWidget);
expect(find.text('Tile 17'), findsOneWidget);
expect(find.text('Tile 18'), findsOneWidget);
expect(find.text('Tile 19'), findsNothing);
// Drags back to beginning and newly added item is visible.
await tester.drag(find.text('Tile 16'), const Offset(0.0, 1000.0));
await tester.pumpAndSettle();
expect(controller.offset, 0.0);
expect(find.text('Tile -1'), findsOneWidget);
expect(find.text('Tile 0'), findsOneWidget);
expect(find.text('Tile 1'), findsOneWidget);
expect(find.text('Tile 2'), findsOneWidget);
expect(find.text('Tile 3'), findsNothing);
});
testWidgets('SliverList should recalculate inaccurate layout offset case 2', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/42142.
final List<int> items = List<int>.generate(20, (int i) => i);
final ScrollController controller = ScrollController();
await tester.pumpWidget(
_buildSliverList(
items: List<int>.from(items),
controller: controller,
itemHeight: 50,
viewportHeight: 200,
),
);
await tester.pumpAndSettle();
await tester.drag(find.text('Tile 2'), const Offset(0.0, -1000.0));
await tester.pumpAndSettle();
// Viewport should be scrolled to the end of list.
expect(controller.offset, 800.0);
expect(find.text('Tile 15'), findsNothing);
expect(find.text('Tile 16'), findsOneWidget);
expect(find.text('Tile 17'), findsOneWidget);
expect(find.text('Tile 18'), findsOneWidget);
expect(find.text('Tile 19'), findsOneWidget);
// Reorders item to the front. This should make item 19 to be first child
// with layout offset = null.
final int swap = items[19];
items[19] = items[3];
items[3] = swap;
await tester.pumpWidget(
_buildSliverList(
items: List<int>.from(items),
controller: controller,
itemHeight: 50,
viewportHeight: 200,
),
);
await tester.pump();
// We need second pump to ensure the scheduled animation gets run.
await tester.pumpAndSettle();
// Scroll offset should stay the same
expect(controller.offset, 800.0);
expect(find.text('Tile 14'), findsNothing);
expect(find.text('Tile 15'), findsNothing);
expect(find.text('Tile 16'), findsOneWidget);
expect(find.text('Tile 17'), findsOneWidget);
expect(find.text('Tile 18'), findsOneWidget);
expect(find.text('Tile 3'), findsOneWidget);
});
testWidgets('SliverList should start to perform layout from the initial child when there is no valid offset', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/66198.
bool isShow = true;
final ScrollController controller = ScrollController();
Widget buildSliverList(ScrollController controller) {
return Directionality(
textDirection: TextDirection.ltr,
child: Center(
child: SizedBox(
height: 200,
child: ListView(
controller: controller,
children: <Widget>[
if (isShow)
for (int i = 0; i < 20; i++)
SizedBox(
height: 50,
child: Text('Tile $i'),
),
const SizedBox(), // Use this widget to occupy the position where the offset is 0 when rebuild
const SizedBox(key: Key('key0'), height: 50.0),
const SizedBox(key: Key('key1'), height: 50.0),
],
),
),
),
);
}
await tester.pumpWidget(buildSliverList(controller));
await tester.pumpAndSettle();
// Scrolling to the bottom.
await tester.drag(find.text('Tile 2'), const Offset(0.0, -1000.0));
await tester.pumpAndSettle();
// Viewport should be scrolled to the end of list.
expect(controller.offset, 900.0);
expect(find.text('Tile 17'), findsNothing);
expect(find.text('Tile 18'), findsOneWidget);
expect(find.text('Tile 19'), findsOneWidget);
expect(find.byKey(const Key('key0')), findsOneWidget);
expect(find.byKey(const Key('key1')), findsOneWidget);
// Trigger rebuild.
isShow = false;
await tester.pumpWidget(buildSliverList(controller));
// After rebuild, [ContainerRenderObjectMixin] has two children, and
// neither of them has a valid layout offset.
// SliverList can layout normally without any assert or dead loop.
// Only the 'SizeBox' show in the viewport.
expect(controller.offset, 0.0);
expect(find.text('Tile 0'), findsNothing);
expect(find.text('Tile 19'), findsNothing);
expect(find.byKey(const Key('key0')), findsOneWidget);
expect(find.byKey(const Key('key1')), findsOneWidget);
});
}
Widget _buildSliverListRenderWidgetChild(List<String> items) {
return MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr,
child: Material(
child: SizedBox(
height: 500,
child: CustomScrollView(
controller: ScrollController(),
slivers: <Widget>[
SliverList(
delegate: SliverChildListDelegate(
items.map<Widget>((String item) {
return Chip(
key: Key(item),
label: Text('Tile $item'),
);
}).toList(),
),
),
],
),
),
),
),
);
}
Widget _buildSliverList({
List<int> items = const <int>[],
ScrollController? controller,
double itemHeight = 500.0,
double viewportHeight = 300.0,
}) {
return Directionality(
textDirection: TextDirection.ltr,
child: Center(
child: SizedBox(
height: viewportHeight,
child: CustomScrollView(
controller: controller,
slivers: <Widget>[
SliverList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int i) {
return SizedBox(
key: ValueKey<int>(items[i]),
height: itemHeight,
child: Text('Tile ${items[i]}'),
);
},
findChildIndexCallback: (Key key) {
final ValueKey<int> valueKey = key as ValueKey<int>;
final int index = items.indexOf(valueKey.value);
return index == -1 ? null : index;
},
childCount: items.length,
),
),
],
),
),
),
);
}