Files
FlClash/test/widgets/keyed_animated_list_test.dart
T
chen08209 8378c7081c feat(views): rework the proxies and scan pages, lay out records by their parts and unify search
A queued delay test shows a static clock until the task pool runs it, so
only the running tests animate while the list scrolls.

Changelog: Hide timed-out nodes and search every list
2026-10-02 20:32:42 +08:00

126 lines
3.9 KiB
Dart

import 'package:fl_clash/widgets/keyed_animated_list.dart';
import 'package:material_ui/material_ui.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
Widget buildList(List<String> items) {
return MaterialApp(
home: Scaffold(
body: KeyedAnimatedList<String>(
items: items,
keyOf: (item) => item,
separator: const Divider(height: 0),
itemBuilder: (_, item) => SizedBox(
key: ValueKey('row-$item'),
height: 40,
child: Text(item),
),
),
),
);
}
double topOf(WidgetTester tester, String item) {
return tester.getTopLeft(find.byKey(ValueKey('row-$item'))).dy;
}
testWidgets('removed item collapses before leaving the tree', (tester) async {
await tester.pumpWidget(buildList(const ['a', 'b', 'c']));
await tester.pumpWidget(buildList(const ['a', 'c']));
await tester.pump();
expect(find.text('b'), findsOneWidget);
final midway = topOf(tester, 'c');
await tester.pump(const Duration(milliseconds: 150));
expect(topOf(tester, 'c'), lessThan(midway));
await tester.pumpAndSettle();
expect(find.text('b'), findsNothing);
expect(topOf(tester, 'c'), 40);
});
testWidgets('reordered item slides from its old slot', (tester) async {
await tester.pumpWidget(buildList(const ['a', 'b', 'c']));
expect(topOf(tester, 'c'), 80);
await tester.pumpWidget(buildList(const ['c', 'a', 'b']));
await tester.pump();
final render = tester.renderObject(find.byKey(const ValueKey('row-c')));
Offset paintedTop() => (render as RenderBox).localToGlobal(Offset.zero);
expect(paintedTop().dy, closeTo(80, 0.01));
await tester.pump(const Duration(milliseconds: 150));
final midway = paintedTop().dy;
expect(midway, greaterThan(0));
expect(midway, lessThan(80));
await tester.pumpAndSettle();
expect(paintedTop().dy, closeTo(0, 0.01));
expect(tester.takeException(), null);
});
testWidgets('item inserted and removed before it grows is dropped', (
tester,
) async {
await tester.pumpWidget(buildList(const ['a']));
await tester.pumpWidget(buildList(const ['a', 'b']));
await tester.pumpWidget(buildList(const ['a']));
expect(tester.takeException(), null);
await tester.pumpAndSettle();
expect(find.text('b'), findsNothing);
expect(find.text('a'), findsOneWidget);
});
testWidgets('item removed and re-added keeps a single row', (tester) async {
await tester.pumpWidget(buildList(const ['a', 'b']));
await tester.pumpWidget(buildList(const ['a']));
await tester.pump(const Duration(milliseconds: 100));
await tester.pumpWidget(buildList(const ['a', 'b']));
await tester.pumpAndSettle();
expect(find.text('b'), findsOneWidget);
expect(topOf(tester, 'b'), 40);
});
testWidgets('rows entering in one update share a single ticker', (
tester,
) async {
final visible = [for (var i = 0; i < 30; i++) '$i'];
await tester.pumpWidget(buildList(visible));
await tester.pumpWidget(
buildList([...visible, for (var i = 30; i < 70; i++) '$i']),
);
await tester.pump();
expect(tester.binding.transientCallbackCount, 1);
await tester.pumpAndSettle();
expect(tester.binding.transientCallbackCount, 0);
});
testWidgets('a change too large to animate swaps the rows at once', (
tester,
) async {
final many = [for (var i = 0; i < 100; i++) '$i'];
await tester.pumpWidget(buildList(many));
await tester.pumpWidget(buildList(const ['0', '1']));
await tester.pump();
expect(
find.descendant(
of: find.byType(KeyedAnimatedList<String>),
matching: find.byType(SizeTransition),
),
findsNothing,
);
expect(find.text('2'), findsNothing);
expect(topOf(tester, '1'), 40);
await tester.pumpWidget(buildList(many));
await tester.pump();
expect(find.text('10'), findsOneWidget);
expect(topOf(tester, '10'), 400);
});
}