Huy 552708f399
Fix test cross-imports for ListTile (#180572)
- A part of https://github.com/flutter/flutter/issues/177415
- In this PR:
- Build a minimal TestListTile widget in `list_tile_test_utils.dart`.
It's basically a Container with necessary properties like min height,
padding, onTap callback with `HitTestBehavior.opaque` to illustrate
InkWell used in ListTile.
- Replace material ListTile widget with TestListTile for all appearances
in `packages/flutter/test/widgets`

## Pre-launch Checklist

- [x] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [x] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [x] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [x] I signed the [CLA].
- [x] I listed at least one issue that this PR fixes in the description
above.
- [x] I updated/added relevant documentation (doc comments with `///`).
- [x] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [x] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [x] All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel
on [Discord].

**Note**: The Flutter team is currently trialing the use of [Gemini Code
Assist for
GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code).
Comments from the `gemini-code-assist` bot should not be taken as
authoritative feedback from the Flutter team. If you find its comments
useful you can update your code accordingly, but if you are unsure or
disagree with the feedback, please feel free to wait for a Flutter team
member's review for guidance on which automated comments should be
addressed.

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md

Signed-off-by: huycozy <huy@nevercode.io>
2026-01-20 03:16:22 +00:00

1763 lines
56 KiB
Dart

// 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/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
import 'list_tile_test_utils.dart';
import 'semantics_tester.dart';
Future<void> test(WidgetTester tester, double offset, {double anchor = 0.0}) {
final viewportOffset = ViewportOffset.fixed(offset);
addTearDown(viewportOffset.dispose);
return tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Viewport(
anchor: anchor / 600.0,
offset: viewportOffset,
slivers: const <Widget>[
SliverToBoxAdapter(child: SizedBox(height: 400.0)),
SliverToBoxAdapter(child: SizedBox(height: 400.0)),
SliverToBoxAdapter(child: SizedBox(height: 400.0)),
SliverToBoxAdapter(child: SizedBox(height: 400.0)),
SliverToBoxAdapter(child: SizedBox(height: 400.0)),
],
),
),
);
}
Future<void> testSliverFixedExtentList(WidgetTester tester, List<String> items) {
return tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: CustomScrollView(
slivers: <Widget>[
SliverFixedExtentList.builder(
itemExtent: 900,
itemCount: items.length,
itemBuilder: (BuildContext context, int index) {
return Center(key: ValueKey<String>(items[index]), child: KeepAlive(items[index]));
},
findChildIndexCallback: (Key key) {
final valueKey = key as ValueKey<String>;
return items.indexOf(valueKey.value);
},
),
],
),
),
);
}
void verify(WidgetTester tester, List<Offset> idealPositions, List<bool> idealVisibles) {
final List<Offset> actualPositions = tester
.renderObjectList<RenderBox>(find.byType(SizedBox, skipOffstage: false))
.map<Offset>((RenderBox target) => target.localToGlobal(Offset.zero))
.toList();
final List<bool> actualVisibles = tester
.renderObjectList<RenderSliverToBoxAdapter>(
find.byType(SliverToBoxAdapter, skipOffstage: false),
)
.map<bool>((RenderSliverToBoxAdapter target) => target.geometry!.visible)
.toList();
expect(actualPositions, equals(idealPositions));
expect(actualVisibles, equals(idealVisibles));
}
void main() {
testWidgets('Viewport basic test', (WidgetTester tester) async {
await test(tester, 0.0);
expect(
tester.renderObject<RenderBox>(find.byType(Viewport)).size,
equals(const Size(800.0, 600.0)),
);
verify(
tester,
<Offset>[
Offset.zero,
const Offset(0.0, 400.0),
const Offset(0.0, 800.0),
const Offset(0.0, 1200.0),
const Offset(0.0, 1600.0),
],
<bool>[true, true, false, false, false],
);
await test(tester, 200.0);
verify(
tester,
<Offset>[
const Offset(0.0, -200.0),
const Offset(0.0, 200.0),
const Offset(0.0, 600.0),
const Offset(0.0, 1000.0),
const Offset(0.0, 1400.0),
],
<bool>[true, true, false, false, false],
);
await test(tester, 600.0);
verify(
tester,
<Offset>[
const Offset(0.0, -600.0),
const Offset(0.0, -200.0),
const Offset(0.0, 200.0),
const Offset(0.0, 600.0),
const Offset(0.0, 1000.0),
],
<bool>[false, true, true, false, false],
);
await test(tester, 900.0);
verify(
tester,
<Offset>[
const Offset(0.0, -900.0),
const Offset(0.0, -500.0),
const Offset(0.0, -100.0),
const Offset(0.0, 300.0),
const Offset(0.0, 700.0),
],
<bool>[false, false, true, true, false],
);
});
testWidgets('Viewport anchor test', (WidgetTester tester) async {
await test(tester, 0.0, anchor: 100.0);
expect(
tester.renderObject<RenderBox>(find.byType(Viewport)).size,
equals(const Size(800.0, 600.0)),
);
verify(
tester,
<Offset>[
const Offset(0.0, 100.0),
const Offset(0.0, 500.0),
const Offset(0.0, 900.0),
const Offset(0.0, 1300.0),
const Offset(0.0, 1700.0),
],
<bool>[true, true, false, false, false],
);
await test(tester, 200.0, anchor: 100.0);
verify(
tester,
<Offset>[
const Offset(0.0, -100.0),
const Offset(0.0, 300.0),
const Offset(0.0, 700.0),
const Offset(0.0, 1100.0),
const Offset(0.0, 1500.0),
],
<bool>[true, true, false, false, false],
);
await test(tester, 600.0, anchor: 100.0);
verify(
tester,
<Offset>[
const Offset(0.0, -500.0),
const Offset(0.0, -100.0),
const Offset(0.0, 300.0),
const Offset(0.0, 700.0),
const Offset(0.0, 1100.0),
],
<bool>[false, true, true, false, false],
);
await test(tester, 900.0, anchor: 100.0);
verify(
tester,
<Offset>[
const Offset(0.0, -800.0),
const Offset(0.0, -400.0),
Offset.zero,
const Offset(0.0, 400.0),
const Offset(0.0, 800.0),
],
<bool>[false, false, true, true, false],
);
});
testWidgets('Multiple grids and lists', (WidgetTester tester) async {
await tester.pumpWidget(
Center(
child: SizedBox(
width: 44.4,
height: 60.0,
child: Directionality(
textDirection: TextDirection.ltr,
child: CustomScrollView(
slivers: <Widget>[
SliverList.list(
children: const <Widget>[
SizedBox(height: 22.2, child: Text('TOP')),
SizedBox(height: 22.2),
SizedBox(height: 22.2),
],
),
SliverFixedExtentList.list(
itemExtent: 22.2,
children: const <Widget>[SizedBox(), Text('A'), SizedBox()],
),
SliverGrid(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 2),
delegate: SliverChildListDelegate(const <Widget>[
SizedBox(),
Text('B'),
SizedBox(),
]),
),
SliverList.list(
children: const <Widget>[
SizedBox(height: 22.2),
SizedBox(height: 22.2),
SizedBox(height: 22.2, child: Text('BOTTOM')),
],
),
],
),
),
),
),
);
final TestGesture gesture = await tester.startGesture(const Offset(400.0, 300.0));
expect(find.text('TOP'), findsOneWidget);
expect(find.text('A'), findsNothing);
expect(find.text('B'), findsNothing);
expect(find.text('BOTTOM'), findsNothing);
await gesture.moveBy(const Offset(0.0, -70.0));
await tester.pump();
expect(find.text('TOP'), findsNothing);
expect(find.text('A'), findsOneWidget);
expect(find.text('B'), findsNothing);
expect(find.text('BOTTOM'), findsNothing);
await gesture.moveBy(const Offset(0.0, -70.0));
await tester.pump();
expect(find.text('TOP'), findsNothing);
expect(find.text('A'), findsNothing);
expect(find.text('B'), findsOneWidget);
expect(find.text('BOTTOM'), findsNothing);
await gesture.moveBy(const Offset(0.0, -70.0));
await tester.pump();
expect(find.text('TOP'), findsNothing);
expect(find.text('A'), findsNothing);
expect(find.text('B'), findsNothing);
expect(find.text('BOTTOM'), findsOneWidget);
});
testWidgets('Sliver grid can replace intermediate items', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/138749.
// The bug happens when items in between first and last item changed while
// the sliver layout only display a item in the middle of the list.
final items = <int>[0, 1, 2, 3, 4, 5];
final replacedItems = <int>[0, 2, 9, 10, 11, 12, 5];
Future<void> pumpSliverGrid(bool replace) async {
await tester.pumpWidget(
Center(
child: SizedBox(
width: 200,
height: 200,
child: Directionality(
textDirection: TextDirection.ltr,
child: CustomScrollView(
slivers: <Widget>[
SliverGrid.builder(
gridDelegate: TestGridDelegate(replace),
itemCount: replace ? 7 : 6,
itemBuilder: (BuildContext context, int index) {
final int item = replace ? replacedItems[index] : items[index];
return Container(
key: ValueKey<int>(item),
alignment: Alignment.center,
child: Text('item $item'),
);
},
findChildIndexCallback: (Key key) {
final int item = (key as ValueKey<int>).value;
final int index = replace ? replacedItems.indexOf(item) : items.indexOf(item);
return index >= 0 ? index : null;
},
),
],
),
),
),
),
);
}
await pumpSliverGrid(false);
expect(find.text('item 0'), findsOneWidget);
expect(find.text('item 1'), findsOneWidget);
expect(find.text('item 2'), findsOneWidget);
expect(find.text('item 3'), findsOneWidget);
expect(find.text('item 4'), findsOneWidget);
await pumpSliverGrid(true);
// The TestGridDelegate only show child at index 1 when not expand.
expect(find.text('item 0'), findsNothing);
expect(find.text('item 1'), findsNothing);
expect(find.text('item 2'), findsOneWidget);
expect(find.text('item 3'), findsNothing);
expect(find.text('item 4'), findsNothing);
});
testWidgets('SliverFixedExtentList correctly clears garbage', (WidgetTester tester) async {
final items = <String>['1', '2', '3', '4', '5', '6'];
await testSliverFixedExtentList(tester, items);
// Keep alive widgets require 1 frame to notify their parents. Pumps in between
// drags to ensure widgets are kept alive.
await tester.drag(find.byType(CustomScrollView), const Offset(0.0, -1200.0));
await tester.pump();
await tester.drag(find.byType(CustomScrollView), const Offset(0.0, -1200.0));
await tester.pump();
await tester.drag(find.byType(CustomScrollView), const Offset(0.0, -800.0));
await tester.pump();
expect(find.text('1'), findsNothing);
expect(find.text('2'), findsNothing);
expect(find.text('3'), findsNothing);
expect(find.text('4'), findsOneWidget);
expect(find.text('5'), findsOneWidget);
// Indexes [0, 1, 2] are kept alive and [3, 4] are in viewport, thus the sliver
// will need to keep updating the elements at these indexes whenever a rebuild is
// triggered. The current child list in RenderSliverFixedExtentList is
// '4' -> '5' -> null.
//
// With the insertion below, all items will get shifted back 1 position. The sliver
// will have to update indexes [0, 1, 2, 3, 4, 5]. Since this is the first time
// item '0' gets initialized, mounting the element will cause it to attach to
// child list in RenderSliverFixedExtentList. This will create a gap.
// '0' -> '4' -> '5' -> null.
items.insert(0, '0');
await testSliverFixedExtentList(tester, items);
// Sliver should collect leading and trailing garbage correctly.
//
// The child list update should occur in following order.
// '0' -> '4' -> '5' -> null Started with Original list.
// '4' -> null Removed 1 leading garbage and 1 trailing garbage.
// '3' -> '4' -> null Prepended '3' because viewport is still at [3, 4].
expect(find.text('0'), findsNothing);
expect(find.text('1'), findsNothing);
expect(find.text('2'), findsNothing);
expect(find.text('3'), findsOneWidget);
expect(find.text('4'), findsOneWidget);
});
testWidgets('SliverFixedExtentList handles underflow when its children changes', (
WidgetTester tester,
) async {
final items = <String>['1', '2', '3', '4', '5', '6'];
final initializedChild = <String>[];
var children = <Widget>[
for (final String item in items)
StateInitSpy(item, () => initializedChild.add(item), key: ValueKey<String>(item)),
];
final controller = ScrollController(initialScrollOffset: 5400);
addTearDown(controller.dispose);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: CustomScrollView(
controller: controller,
slivers: <Widget>[SliverFixedExtentList.list(itemExtent: 900, children: children)],
),
),
);
await tester.pumpAndSettle();
expect(find.text('1'), findsNothing);
expect(find.text('2'), findsNothing);
expect(find.text('3'), findsNothing);
expect(find.text('4'), findsNothing);
expect(find.text('5'), findsNothing);
expect(find.text('6'), findsOneWidget);
expect(listEquals<String>(initializedChild, <String>['6']), isTrue);
// move to item 1 and swap the children at the same time
controller.jumpTo(0);
final Widget temp = children[5];
children[5] = children[0];
children[0] = temp;
children = List<Widget>.of(children);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: CustomScrollView(
controller: controller,
slivers: <Widget>[SliverFixedExtentList.list(itemExtent: 900, children: children)],
),
),
);
expect(find.text('1'), findsNothing);
expect(find.text('2'), findsNothing);
expect(find.text('3'), findsNothing);
expect(find.text('4'), findsNothing);
expect(find.text('5'), findsNothing);
expect(find.text('6'), findsOneWidget);
// None of the children should be built.
expect(listEquals<String>(initializedChild, <String>['6']), isTrue);
});
testWidgets('SliverGrid Correctly layout children after rearranging', (
WidgetTester tester,
) async {
await tester.pumpWidget(
const TestSliverGrid(<Widget>[Text('item0', key: Key('0')), Text('item1', key: Key('1'))]),
);
await tester.pumpWidget(
const TestSliverGrid(<Widget>[
Text('item0', key: Key('0')),
Text('item3', key: Key('3')),
Text('item4', key: Key('4')),
Text('item1', key: Key('1')),
]),
);
expect(find.text('item0'), findsOneWidget);
expect(find.text('item3'), findsOneWidget);
expect(find.text('item4'), findsOneWidget);
expect(find.text('item1'), findsOneWidget);
final Offset item0Location = tester.getCenter(find.text('item0'));
final Offset item3Location = tester.getCenter(find.text('item3'));
final Offset item4Location = tester.getCenter(find.text('item4'));
final Offset item1Location = tester.getCenter(find.text('item1'));
expect(
isRight(item0Location, item3Location) && sameHorizontal(item0Location, item3Location),
true,
);
expect(
isBelow(item0Location, item4Location) && sameVertical(item0Location, item4Location),
true,
);
expect(isBelow(item0Location, item1Location) && isRight(item0Location, item1Location), true);
});
testWidgets('SliverGrid negative usableCrossAxisExtent', (WidgetTester tester) async {
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Center(
child: SizedBox(
width: 4,
height: 4,
child: CustomScrollView(
slivers: <Widget>[
SliverGrid(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 8,
mainAxisSpacing: 8,
),
delegate: SliverChildListDelegate(<Widget>[
const Center(child: Text('A')),
const Center(child: Text('B')),
const Center(child: Text('C')),
const Center(child: Text('D')),
]),
),
],
),
),
),
),
);
expect(tester.takeException(), isNull);
});
testWidgets('SliverList can handle inaccurate scroll offset due to changes in children list', (
WidgetTester tester,
) async {
// Regression test for https://github.com/flutter/flutter/pull/59888.
var skip = true;
Widget buildItem(BuildContext context, int index) {
return !skip || index.isEven
? Card(
child: TestListTile(title: Text('item$index', style: const TextStyle(fontSize: 80))),
)
: Container();
}
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(useMaterial3: false),
home: Scaffold(
body: CustomScrollView(
slivers: <Widget>[SliverList.builder(itemCount: 30, itemBuilder: buildItem)],
),
),
),
);
// Only even items 0~12 are on the screen.
expect(find.text('item0'), findsOneWidget);
expect(find.text('item12'), findsOneWidget);
expect(find.text('item14'), findsNothing);
await tester.drag(find.byType(CustomScrollView), const Offset(0.0, -750.0));
await tester.pump();
// Only even items 16~28 are on the screen.
expect(find.text('item15'), findsNothing);
expect(find.text('item16'), findsOneWidget);
expect(find.text('item28'), findsOneWidget);
skip = false;
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: CustomScrollView(
slivers: <Widget>[SliverList.builder(itemCount: 30, itemBuilder: buildItem)],
),
),
),
);
// Only items 12~19 are on the screen.
expect(find.text('item11'), findsNothing);
expect(find.text('item12'), findsOneWidget);
expect(find.text('item19'), findsOneWidget);
expect(find.text('item20'), findsNothing);
await tester.drag(find.byType(CustomScrollView), const Offset(0.0, 250.0));
await tester.pump();
// Only items 10~16 are on the screen.
expect(find.text('item9'), findsNothing);
expect(find.text('item10'), findsOneWidget);
expect(find.text('item16'), findsOneWidget);
expect(find.text('item17'), findsNothing);
// The inaccurate scroll offset should reach zero at this point
await tester.drag(find.byType(CustomScrollView), const Offset(0.0, 250.0));
await tester.pump();
// Only items 7~13 are on the screen.
expect(find.text('item6'), findsNothing);
expect(find.text('item7'), findsOneWidget);
expect(find.text('item13'), findsOneWidget);
expect(find.text('item14'), findsNothing);
// It will be corrected as we scroll, so we have to drag multiple times.
await tester.drag(find.byType(CustomScrollView), const Offset(0.0, 250.0));
await tester.pump();
await tester.drag(find.byType(CustomScrollView), const Offset(0.0, 250.0));
await tester.pump();
await tester.drag(find.byType(CustomScrollView), const Offset(0.0, 250.0));
await tester.pump();
// Only items 0~6 are on the screen.
expect(find.text('item0'), findsOneWidget);
expect(find.text('item6'), findsOneWidget);
expect(find.text('item7'), findsNothing);
});
testWidgets('SliverFixedExtentList Correctly layout children after rearranging', (
WidgetTester tester,
) async {
await tester.pumpWidget(
const TestSliverFixedExtentList(<Widget>[
Text('item0', key: Key('0')),
Text('item2', key: Key('2')),
Text('item1', key: Key('1')),
]),
);
await tester.pumpWidget(
const TestSliverFixedExtentList(<Widget>[
Text('item0', key: Key('0')),
Text('item3', key: Key('3')),
Text('item1', key: Key('1')),
Text('item4', key: Key('4')),
Text('item2', key: Key('2')),
]),
);
expect(find.text('item0'), findsOneWidget);
expect(find.text('item3'), findsOneWidget);
expect(find.text('item1'), findsOneWidget);
expect(find.text('item4'), findsOneWidget);
expect(find.text('item2'), findsOneWidget);
final Offset item0Location = tester.getCenter(find.text('item0'));
final Offset item3Location = tester.getCenter(find.text('item3'));
final Offset item1Location = tester.getCenter(find.text('item1'));
final Offset item4Location = tester.getCenter(find.text('item4'));
final Offset item2Location = tester.getCenter(find.text('item2'));
expect(
isBelow(item0Location, item3Location) && sameVertical(item0Location, item3Location),
true,
);
expect(
isBelow(item3Location, item1Location) && sameVertical(item3Location, item1Location),
true,
);
expect(
isBelow(item1Location, item4Location) && sameVertical(item1Location, item4Location),
true,
);
expect(
isBelow(item4Location, item2Location) && sameVertical(item4Location, item2Location),
true,
);
});
testWidgets('Can override ErrorWidget.build', (WidgetTester tester) async {
const errorText = Text('error');
final ErrorWidgetBuilder oldBuilder = ErrorWidget.builder;
ErrorWidget.builder = (FlutterErrorDetails details) => errorText;
final builderThrowsDelegate = SliverChildBuilderDelegate(
(_, _) => throw 'builder',
addAutomaticKeepAlives: false,
addRepaintBoundaries: false,
addSemanticIndexes: false,
);
final wrapped = builderThrowsDelegate.build(_NullBuildContext(), 0)! as KeyedSubtree;
expect(wrapped.child, errorText);
expect(tester.takeException(), 'builder');
ErrorWidget.builder = oldBuilder;
});
testWidgets(
'SliverFixedExtentList with SliverChildBuilderDelegate auto-correct scroll offset - super fast',
(WidgetTester tester) async {
final controller = ScrollController(initialScrollOffset: 600);
addTearDown(controller.dispose);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: CustomScrollView(
controller: controller,
cacheExtent: 0,
slivers: <Widget>[
SliverFixedExtentList.builder(
itemExtent: 200,
itemBuilder: (BuildContext context, int index) {
if (index <= 6) {
return Center(child: Text('Page $index'));
}
return null;
},
),
],
),
),
);
expect(find.text('Page 0'), findsNothing);
expect(find.text('Page 6'), findsNothing);
await tester.drag(find.text('Page 5'), const Offset(0, -1000));
// Controller will be temporarily over-scrolled (before the frame triggered by the drag) because
// SliverFixedExtentList doesn't report its size until it has built its last child, so the
// maxScrollExtent is infinite, so when we move by 1000 pixels in one go, we go all the way.
//
// This never actually gets rendered, it's just the controller state before we lay out.
expect(controller.offset, 1600.0);
// However, once we pump, the scroll offset gets clamped to the newly discovered maximum, which
// is the itemExtent (200) times the number of items (7) minus the height of the viewport (600).
// This adds up to 800.0.
await tester.pump();
expect(find.text('Page 0'), findsNothing);
expect(find.text('Page 6'), findsOneWidget);
expect(controller.offset, 800.0);
expect(await tester.pumpAndSettle(), 1); // there should be no animation here
expect(controller.offset, 800.0);
},
);
testWidgets(
'SliverFixedExtentList with SliverChildBuilderDelegate auto-correct scroll offset - reasonable',
(WidgetTester tester) async {
final controller = ScrollController(initialScrollOffset: 600);
addTearDown(controller.dispose);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: CustomScrollView(
controller: controller,
cacheExtent: 0,
slivers: <Widget>[
SliverFixedExtentList.builder(
itemExtent: 200,
itemBuilder: (BuildContext context, int index) {
if (index <= 6) {
return Center(child: Text('Page $index'));
}
return null;
},
),
],
),
),
);
await tester.drag(find.text('Page 5'), const Offset(0, -210));
// Controller will be temporarily over-scrolled.
expect(controller.offset, 810.0);
await tester.pumpAndSettle();
// It will be corrected after a auto scroll animation.
expect(controller.offset, 800.0);
},
);
Widget boilerPlate(List<Widget> slivers) {
return Localizations(
locale: const Locale('en', 'us'),
delegates: const <LocalizationsDelegate<dynamic>>[
DefaultWidgetsLocalizations.delegate,
DefaultMaterialLocalizations.delegate,
],
child: Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: const MediaQueryData(),
child: CustomScrollView(slivers: slivers),
),
),
);
}
group('SliverOffstage - ', () {
testWidgets('offstage true', (WidgetTester tester) async {
final semantics = SemanticsTester(tester);
await tester.pumpWidget(
boilerPlate(<Widget>[const SliverOffstage(sliver: SliverToBoxAdapter(child: Text('a')))]),
);
expect(semantics.nodesWith(label: 'a'), hasLength(0));
expect(find.byType(Text), findsNothing);
final RenderViewport renderViewport = tester.renderObject(find.byType(Viewport));
final RenderSliver renderSliver = renderViewport.lastChild!;
expect(renderSliver.geometry!.scrollExtent, 0.0);
expect(find.byType(SliverOffstage), findsNothing);
semantics.dispose();
});
testWidgets('offstage false', (WidgetTester tester) async {
final semantics = SemanticsTester(tester);
await tester.pumpWidget(
boilerPlate(<Widget>[
const SliverOffstage(offstage: false, sliver: SliverToBoxAdapter(child: Text('a'))),
]),
);
expect(semantics.nodesWith(label: 'a'), hasLength(1));
expect(find.byType(Text), findsOneWidget);
final RenderViewport renderViewport = tester.renderObject(find.byType(Viewport));
final RenderSliver renderSliver = renderViewport.lastChild!;
expect(renderSliver.geometry!.scrollExtent, 14.0);
expect(find.byType(SliverOffstage), paints..paragraph());
semantics.dispose();
});
});
group('SliverOpacity - ', () {
testWidgets('painting & semantics', (WidgetTester tester) async {
final semantics = SemanticsTester(tester);
// Opacity 1.0: Semantics and painting
await tester.pumpWidget(
boilerPlate(<Widget>[
const SliverOpacity(
sliver: SliverToBoxAdapter(child: Text('a', textDirection: TextDirection.rtl)),
opacity: 1.0,
),
]),
);
expect(semantics.nodesWith(label: 'a'), hasLength(1));
expect(find.byType(SliverOpacity), paints..paragraph());
// Opacity 0.0: Nothing
await tester.pumpWidget(
boilerPlate(<Widget>[
const SliverOpacity(
sliver: SliverToBoxAdapter(child: Text('a', textDirection: TextDirection.rtl)),
opacity: 0.0,
),
]),
);
expect(semantics.nodesWith(label: 'a'), hasLength(0));
expect(find.byType(SliverOpacity), paintsNothing);
// Opacity 0.0 with semantics: Just semantics
await tester.pumpWidget(
boilerPlate(<Widget>[
const SliverOpacity(
sliver: SliverToBoxAdapter(child: Text('a', textDirection: TextDirection.rtl)),
opacity: 0.0,
alwaysIncludeSemantics: true,
),
]),
);
expect(semantics.nodesWith(label: 'a'), hasLength(1));
expect(find.byType(SliverOpacity), paintsNothing);
// Opacity 0.0 without semantics: Nothing
await tester.pumpWidget(
boilerPlate(<Widget>[
const SliverOpacity(
sliver: SliverToBoxAdapter(child: Text('a', textDirection: TextDirection.rtl)),
opacity: 0.0,
),
]),
);
expect(semantics.nodesWith(label: 'a'), hasLength(0));
expect(find.byType(SliverOpacity), paintsNothing);
// Opacity 0.1: Semantics and painting
await tester.pumpWidget(
boilerPlate(<Widget>[
const SliverOpacity(
sliver: SliverToBoxAdapter(child: Text('a', textDirection: TextDirection.rtl)),
opacity: 0.1,
),
]),
);
expect(semantics.nodesWith(label: 'a'), hasLength(1));
expect(find.byType(SliverOpacity), paints..paragraph());
// Opacity 0.1 without semantics: Still has semantics and painting
await tester.pumpWidget(
boilerPlate(<Widget>[
const SliverOpacity(
sliver: SliverToBoxAdapter(child: Text('a', textDirection: TextDirection.rtl)),
opacity: 0.1,
),
]),
);
expect(semantics.nodesWith(label: 'a'), hasLength(1));
expect(find.byType(SliverOpacity), paints..paragraph());
// Opacity 0.1 with semantics: Semantics and painting
await tester.pumpWidget(
boilerPlate(<Widget>[
const SliverOpacity(
sliver: SliverToBoxAdapter(child: Text('a', textDirection: TextDirection.rtl)),
opacity: 0.1,
alwaysIncludeSemantics: true,
),
]),
);
expect(semantics.nodesWith(label: 'a'), hasLength(1));
expect(find.byType(SliverOpacity), paints..paragraph());
semantics.dispose();
});
});
group('SliverIgnorePointer - ', () {
testWidgets('ignores pointer events', (WidgetTester tester) async {
final semantics = SemanticsTester(tester);
final events = <String>[];
await tester.pumpWidget(
boilerPlate(<Widget>[
SliverIgnorePointer(
ignoringSemantics: false,
sliver: SliverToBoxAdapter(
child: GestureDetector(
child: const Text('a'),
onTap: () {
events.add('tap');
},
),
),
),
]),
);
expect(semantics.nodesWith(label: 'a'), hasLength(1));
await tester.tap(find.byType(GestureDetector), warnIfMissed: false);
expect(events, equals(<String>[]));
semantics.dispose();
});
testWidgets('ignores semantics', (WidgetTester tester) async {
final semantics = SemanticsTester(tester);
final events = <String>[];
await tester.pumpWidget(
boilerPlate(<Widget>[
SliverIgnorePointer(
ignoring: false,
ignoringSemantics: true,
sliver: SliverToBoxAdapter(
child: GestureDetector(
child: const Text('a'),
onTap: () {
events.add('tap');
},
),
),
),
]),
);
expect(semantics.nodesWith(label: 'a'), hasLength(0));
await tester.tap(find.byType(GestureDetector));
expect(events, equals(<String>['tap']));
semantics.dispose();
});
testWidgets('ignoring only block semantics actions', (WidgetTester tester) async {
final semantics = SemanticsTester(tester);
await tester.pumpWidget(
boilerPlate(<Widget>[
SliverIgnorePointer(
sliver: SliverToBoxAdapter(
child: GestureDetector(child: const Text('a'), onTap: () {}),
),
),
]),
);
expect(semantics, includesNodeWith(label: 'a', actions: <SemanticsAction>[]));
semantics.dispose();
});
testWidgets('ignores pointer events & semantics', (WidgetTester tester) async {
final semantics = SemanticsTester(tester);
final events = <String>[];
await tester.pumpWidget(
boilerPlate(<Widget>[
SliverIgnorePointer(
ignoringSemantics: true,
sliver: SliverToBoxAdapter(
child: GestureDetector(
child: const Text('a'),
onTap: () {
events.add('tap');
},
),
),
),
]),
);
expect(semantics.nodesWith(label: 'a'), hasLength(0));
await tester.tap(find.byType(GestureDetector), warnIfMissed: false);
expect(events, equals(<String>[]));
semantics.dispose();
});
testWidgets('ignores nothing', (WidgetTester tester) async {
final semantics = SemanticsTester(tester);
final events = <String>[];
await tester.pumpWidget(
boilerPlate(<Widget>[
SliverIgnorePointer(
ignoring: false,
ignoringSemantics: false,
sliver: SliverToBoxAdapter(
child: GestureDetector(
child: const Text('a'),
onTap: () {
events.add('tap');
},
),
),
),
]),
);
expect(semantics.nodesWith(label: 'a'), hasLength(1));
await tester.tap(find.byType(GestureDetector));
expect(events, equals(<String>['tap']));
semantics.dispose();
});
});
group('SliverEnsureSemantics - ', () {
testWidgets('ensure semantics', (WidgetTester tester) async {
final semantics = SemanticsTester(tester);
await tester.pumpWidget(
boilerPlate(<Widget>[
const SliverEnsureSemantics(sliver: SliverToBoxAdapter(child: Text('a'))),
SliverList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return Card(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text('Lorem Ipsum $index'),
),
);
},
childCount: 50,
semanticIndexOffset: 1,
),
),
const SliverEnsureSemantics(sliver: SliverToBoxAdapter(child: Text('b'))),
]),
);
// Even though 'b' is outside of the Viewport and cacheExtent, since it is
// wrapped with a `SliverEnsureSemantics` it will still be included in the
// semantics tree.
expect(semantics.nodesWith(label: 'b'), hasLength(1));
expect(find.text('b'), findsNothing);
expect(find.byType(SliverEnsureSemantics, skipOffstage: false), findsNWidgets(2));
semantics.dispose();
});
});
testWidgets('SliverList handles 0 scrollOffsetCorrection', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/62198
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: CustomScrollView(
physics: const BouncingScrollPhysics(parent: AlwaysScrollableScrollPhysics()),
slivers: <Widget>[
SliverList.list(
children: const <Widget>[SizedBox.shrink(), Text('index 1'), Text('index 2')],
),
],
),
),
),
);
await tester.fling(find.byType(Scrollable), const Offset(0.0, -500.0), 10000.0);
await tester.pumpAndSettle();
expect(tester.takeException(), isNull);
});
testWidgets('SliverGrid children can be arbitrarily placed', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/64006
var firstTapped = 0;
var secondTapped = 0;
final Key key = UniqueKey();
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
key: key,
body: CustomScrollView(
slivers: <Widget>[
SliverGrid(
delegate: SliverChildBuilderDelegate((BuildContext context, int index) {
return Material(
color: index.isEven ? Colors.yellow : Colors.red,
child: InkWell(
onTap: () {
index.isEven ? firstTapped++ : secondTapped++;
},
child: Text('Index $index'),
),
);
}, childCount: 2),
gridDelegate: _TestArbitrarySliverGridDelegate(),
),
],
),
),
),
);
// Assertion not triggered by arbitrary placement
expect(tester.takeException(), isNull);
// Verify correct hit testing
await tester.tap(find.text('Index 0'));
expect(firstTapped, 1);
expect(secondTapped, 0);
await tester.tap(find.text('Index 1'));
expect(firstTapped, 1);
expect(secondTapped, 1);
// Check other places too
final Offset bottomLeft = tester.getBottomLeft(find.byKey(key));
await tester.tapAt(bottomLeft);
expect(firstTapped, 1);
expect(secondTapped, 1);
final Offset topRight = tester.getTopRight(find.byKey(key));
await tester.tapAt(topRight);
expect(firstTapped, 1);
expect(secondTapped, 1);
await tester.tapAt(const Offset(100.0, 100.0));
expect(firstTapped, 1);
expect(secondTapped, 1);
await tester.tapAt(const Offset(700.0, 500.0));
expect(firstTapped, 1);
expect(secondTapped, 1);
});
testWidgets('SliverFixedExtentList.builder should respect semanticIndexOffset', (
WidgetTester tester,
) async {
await tester.pumpWidget(
MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr,
child: SizedBox(
height: 200,
child: CustomScrollView(
slivers: <Widget>[
SliverFixedExtentList.builder(
itemExtent: 50,
itemCount: 3,
semanticIndexOffset: 10,
itemBuilder: (BuildContext context, int index) {
return SizedBox(height: 50, child: Text('Item $index'));
},
),
],
),
),
),
),
);
IndexedSemantics semanticsFor(String text) {
return tester.widget<IndexedSemantics>(
find.ancestor(of: find.text(text), matching: find.byType(IndexedSemantics)).first,
);
}
IndexedSemantics semanticsForItem(int index) => semanticsFor('Item $index');
final IndexedSemantics s0 = semanticsForItem(0);
final IndexedSemantics s1 = semanticsForItem(1);
final IndexedSemantics s2 = semanticsForItem(2);
expect(s0.index, 10);
expect(s1.index, 11);
expect(s2.index, 12);
});
testWidgets('SliverList.builder can build children', (WidgetTester tester) async {
var firstTapped = 0;
var secondTapped = 0;
final Key key = UniqueKey();
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
key: key,
body: CustomScrollView(
slivers: <Widget>[
SliverList.builder(
itemCount: 2,
itemBuilder: (BuildContext context, int index) {
return Material(
color: index.isEven ? Colors.yellow : Colors.red,
child: InkWell(
onTap: () {
index.isEven ? firstTapped++ : secondTapped++;
},
child: Text('Index $index'),
),
);
},
),
],
),
),
),
);
// Verify correct hit testing
await tester.tap(find.text('Index 0'));
expect(firstTapped, 1);
expect(secondTapped, 0);
firstTapped = 0;
await tester.tap(find.text('Index 1'));
expect(firstTapped, 0);
expect(secondTapped, 1);
});
testWidgets('SliverList.builder can build children', (WidgetTester tester) async {
var firstTapped = 0;
var secondTapped = 0;
final Key key = UniqueKey();
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
key: key,
body: CustomScrollView(
slivers: <Widget>[
SliverList.builder(
itemCount: 2,
itemBuilder: (BuildContext context, int index) {
return Material(
color: index.isEven ? Colors.yellow : Colors.red,
child: InkWell(
onTap: () {
index.isEven ? firstTapped++ : secondTapped++;
},
child: Text('Index $index'),
),
);
},
),
],
),
),
),
);
// Verify correct hit testing
await tester.tap(find.text('Index 0'));
expect(firstTapped, 1);
expect(secondTapped, 0);
firstTapped = 0;
await tester.tap(find.text('Index 1'));
expect(firstTapped, 0);
expect(secondTapped, 1);
});
testWidgets('SliverList.separated can build children', (WidgetTester tester) async {
var firstTapped = 0;
var secondTapped = 0;
final Key key = UniqueKey();
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
key: key,
body: CustomScrollView(
slivers: <Widget>[
SliverList.separated(
itemCount: 2,
itemBuilder: (BuildContext context, int index) {
return Material(
color: index.isEven ? Colors.yellow : Colors.red,
child: InkWell(
onTap: () {
index.isEven ? firstTapped++ : secondTapped++;
},
child: Text('Index $index'),
),
);
},
separatorBuilder: (BuildContext context, int index) => Text('Separator $index'),
),
],
),
),
),
);
// Verify correct hit testing
await tester.tap(find.text('Index 0'));
expect(firstTapped, 1);
expect(secondTapped, 0);
firstTapped = 0;
await tester.tap(find.text('Index 1'));
expect(firstTapped, 0);
expect(secondTapped, 1);
});
testWidgets('SliverList.separated has correct number of children', (WidgetTester tester) async {
final Key key = UniqueKey();
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
key: key,
body: CustomScrollView(
slivers: <Widget>[
SliverList.separated(
itemCount: 2,
itemBuilder: (BuildContext context, int index) => const Text('item'),
separatorBuilder: (BuildContext context, int index) => const Text('separator'),
),
],
),
),
),
);
expect(find.text('item'), findsNWidgets(2));
expect(find.text('separator'), findsNWidgets(1));
});
testWidgets('SliverList.list can build children', (WidgetTester tester) async {
var firstTapped = 0;
var secondTapped = 0;
final Key key = UniqueKey();
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
key: key,
body: CustomScrollView(
slivers: <Widget>[
SliverList.list(
children: <Widget>[
Material(
color: Colors.yellow,
child: InkWell(onTap: () => firstTapped++, child: const Text('Index 0')),
),
Material(
color: Colors.red,
child: InkWell(onTap: () => secondTapped++, child: const Text('Index 1')),
),
],
),
],
),
),
),
);
// Verify correct hit testing
await tester.tap(find.text('Index 0'));
expect(firstTapped, 1);
expect(secondTapped, 0);
firstTapped = 0;
await tester.tap(find.text('Index 1'));
expect(firstTapped, 0);
expect(secondTapped, 1);
});
testWidgets('SliverFixedExtentList.builder can build children', (WidgetTester tester) async {
var firstTapped = 0;
var secondTapped = 0;
final Key key = UniqueKey();
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
key: key,
body: CustomScrollView(
slivers: <Widget>[
SliverFixedExtentList.builder(
itemCount: 2,
itemExtent: 100,
itemBuilder: (BuildContext context, int index) {
return Material(
color: index.isEven ? Colors.yellow : Colors.red,
child: InkWell(
onTap: () {
index.isEven ? firstTapped++ : secondTapped++;
},
child: Text('Index $index'),
),
);
},
),
],
),
),
),
);
// Verify correct hit testing
await tester.tap(find.text('Index 0'));
expect(firstTapped, 1);
expect(secondTapped, 0);
firstTapped = 0;
await tester.tap(find.text('Index 1'));
expect(firstTapped, 0);
expect(secondTapped, 1);
});
testWidgets('SliverList.list can build children', (WidgetTester tester) async {
var firstTapped = 0;
var secondTapped = 0;
final Key key = UniqueKey();
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
key: key,
body: CustomScrollView(
slivers: <Widget>[
SliverFixedExtentList.list(
itemExtent: 100,
children: <Widget>[
Material(
color: Colors.yellow,
child: InkWell(onTap: () => firstTapped++, child: const Text('Index 0')),
),
Material(
color: Colors.red,
child: InkWell(onTap: () => secondTapped++, child: const Text('Index 1')),
),
],
),
],
),
),
),
);
// Verify correct hit testing
await tester.tap(find.text('Index 0'));
expect(firstTapped, 1);
expect(secondTapped, 0);
firstTapped = 0;
await tester.tap(find.text('Index 1'));
expect(firstTapped, 0);
expect(secondTapped, 1);
});
testWidgets('SliverGrid.builder can build children', (WidgetTester tester) async {
var firstTapped = 0;
var secondTapped = 0;
final Key key = UniqueKey();
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
key: key,
body: CustomScrollView(
slivers: <Widget>[
SliverGrid.builder(
itemCount: 2,
itemBuilder: (BuildContext context, int index) {
return Material(
color: index.isEven ? Colors.yellow : Colors.red,
child: InkWell(
onTap: () {
index.isEven ? firstTapped++ : secondTapped++;
},
child: Text('Index $index'),
),
);
},
gridDelegate: _TestArbitrarySliverGridDelegate(),
),
],
),
),
),
);
// Verify correct hit testing
await tester.tap(find.text('Index 0'));
expect(firstTapped, 1);
expect(secondTapped, 0);
firstTapped = 0;
await tester.tap(find.text('Index 1'));
expect(firstTapped, 0);
expect(secondTapped, 1);
});
testWidgets('SliverGrid.list can display children', (WidgetTester tester) async {
var firstTapped = 0;
var secondTapped = 0;
final Key key = UniqueKey();
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
key: key,
body: CustomScrollView(
slivers: <Widget>[
SliverGrid.list(
gridDelegate: _TestArbitrarySliverGridDelegate(),
children: <Widget>[
Material(
color: Colors.yellow,
child: InkWell(onTap: () => firstTapped++, child: const Text('First')),
),
Material(
color: Colors.red,
child: InkWell(onTap: () => secondTapped++, child: const Text('Second')),
),
],
),
],
),
),
),
);
// Verify correct hit testing
await tester.tap(find.text('First'));
expect(firstTapped, 1);
expect(secondTapped, 0);
firstTapped = 0;
await tester.tap(find.text('Second'));
expect(firstTapped, 0);
expect(secondTapped, 1);
});
testWidgets('SliverGrid.list with empty children list', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: CustomScrollView(
slivers: <Widget>[
SliverGrid.list(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 2),
children: const <Widget>[],
),
],
),
),
),
);
// Should render without errors - the SliverGrid should be present even with empty children
expect(find.byType(CustomScrollView), findsOneWidget);
});
testWidgets('SliverGrid.builder respects semanticIndexOffset', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: CustomScrollView(
slivers: <Widget>[
SliverGrid.builder(
itemCount: 3,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 2),
semanticIndexOffset: 7,
itemBuilder: (BuildContext context, int index) {
return Center(child: Text('G $index'));
},
),
],
),
),
),
);
IndexedSemantics semanticsFor(String text) {
return tester.widget<IndexedSemantics>(
find.ancestor(of: find.text(text), matching: find.byType(IndexedSemantics)).first,
);
}
IndexedSemantics semanticsForGridItem(int index) => semanticsFor('G $index');
final IndexedSemantics s0 = semanticsForGridItem(0);
final IndexedSemantics s1 = semanticsForGridItem(1);
final IndexedSemantics s2 = semanticsForGridItem(2);
expect(s0.index, 7);
expect(s1.index, 8);
expect(s2.index, 9);
});
testWidgets('SliverGridRegularTileLayout.computeMaxScrollOffset handles 0 children', (
WidgetTester tester,
) async {
// Regression test for https://github.com/flutter/flutter/issues/59663
final controller = ScrollController();
addTearDown(controller.dispose);
// SliverGridDelegateWithFixedCrossAxisCount
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: CustomScrollView(
controller: controller,
slivers: <Widget>[
SliverGrid.builder(
itemCount: 0,
itemBuilder: (_, _) => Container(),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 1,
mainAxisSpacing: 10,
childAspectRatio: 2.1,
),
),
],
),
),
),
);
// Verify correct scroll extent
expect(controller.position.maxScrollExtent, 0.0);
// SliverGridDelegateWithMaxCrossAxisExtent
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: CustomScrollView(
controller: controller,
slivers: <Widget>[
SliverGrid.builder(
itemCount: 0,
itemBuilder: (_, _) => Container(),
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 30,
),
),
],
),
),
),
);
// Verify correct scroll extent
expect(controller.position.maxScrollExtent, 0.0);
});
}
bool isRight(Offset a, Offset b) => b.dx > a.dx;
bool isBelow(Offset a, Offset b) => b.dy > a.dy;
bool sameHorizontal(Offset a, Offset b) => b.dy == a.dy;
bool sameVertical(Offset a, Offset b) => b.dx == a.dx;
class TestSliverGrid extends StatelessWidget {
const TestSliverGrid(this.children, {super.key});
final List<Widget> children;
@override
Widget build(BuildContext context) {
return Directionality(
textDirection: TextDirection.ltr,
child: CustomScrollView(
slivers: <Widget>[
SliverGrid(
delegate: SliverChildListDelegate(children),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 2),
),
],
),
);
}
}
class _TestArbitrarySliverGridDelegate implements SliverGridDelegate {
@override
SliverGridLayout getLayout(SliverConstraints constraints) {
return _TestArbitrarySliverGridLayout();
}
@override
bool shouldRelayout(SliverGridDelegate oldDelegate) {
return false;
}
}
class _TestArbitrarySliverGridLayout implements SliverGridLayout {
@override
double computeMaxScrollOffset(int childCount) => 1000;
@override
int getMinChildIndexForScrollOffset(double scrollOffset) => 0;
@override
int getMaxChildIndexForScrollOffset(double scrollOffset) => 2;
@override
SliverGridGeometry getGeometryForChildIndex(int index) {
return SliverGridGeometry(
scrollOffset: index * 100.0 + 300.0,
crossAxisOffset: 200.0,
mainAxisExtent: 100.0,
crossAxisExtent: 100.0,
);
}
}
class TestSliverFixedExtentList extends StatelessWidget {
const TestSliverFixedExtentList(this.children, {super.key});
final List<Widget> children;
@override
Widget build(BuildContext context) {
return Directionality(
textDirection: TextDirection.ltr,
child: CustomScrollView(
slivers: <Widget>[SliverFixedExtentList.list(itemExtent: 10.0, children: children)],
),
);
}
}
class StateInitSpy extends StatefulWidget {
const StateInitSpy(this.data, this.onStateInit, {super.key});
final String data;
final VoidCallback onStateInit;
@override
StateInitSpyState createState() => StateInitSpyState();
}
class StateInitSpyState extends State<StateInitSpy> {
@override
void initState() {
super.initState();
widget.onStateInit();
}
@override
Widget build(BuildContext context) {
return Text(widget.data);
}
}
class KeepAlive extends StatefulWidget {
const KeepAlive(this.data, {super.key});
final String data;
@override
KeepAliveState createState() => KeepAliveState();
}
class KeepAliveState extends State<KeepAlive> with AutomaticKeepAliveClientMixin {
@override
bool get wantKeepAlive => true;
@override
Widget build(BuildContext context) {
super.build(context);
return Text(widget.data);
}
}
class _NullBuildContext implements BuildContext {
@override
dynamic noSuchMethod(Invocation invocation) => throw UnimplementedError();
}
class TestGridDelegate implements SliverGridDelegate {
TestGridDelegate(this.replace);
final bool replace;
@override
SliverGridLayout getLayout(SliverConstraints constraints) {
return TestGridLayout(replace);
}
@override
bool shouldRelayout(covariant TestGridDelegate oldDelegate) {
return true;
}
}
class TestGridLayout implements SliverGridLayout {
TestGridLayout(this.replace);
final bool replace;
@override
double computeMaxScrollOffset(int childCount) {
return 200;
}
@override
SliverGridGeometry getGeometryForChildIndex(int index) {
return SliverGridGeometry(
crossAxisOffset: 20.0 + 20 * index,
crossAxisExtent: 20,
mainAxisExtent: 20,
scrollOffset: 0,
);
}
@override
int getMaxChildIndexForScrollOffset(double scrollOffset) {
if (replace) {
return 1;
}
return 5;
}
@override
int getMinChildIndexForScrollOffset(double scrollOffset) {
if (replace) {
return 1;
}
return 0;
}
}