mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
- 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>
1763 lines
56 KiB
Dart
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;
|
|
}
|
|
}
|