first commit

This commit is contained in:
2026-07-31 17:09:04 +02:00
commit 747ed57141
8 changed files with 606 additions and 0 deletions
+279
View File
@@ -0,0 +1,279 @@
import "package:flutter/gestures.dart";
import "package:flutter/material.dart";
import "package:flutter_test/flutter_test.dart";
import "package:scroll_forwarder/scroll_forwarder.dart";
void main() {
const double listWidth = 200;
const double listHeight = 400;
const Offset margin = Offset(10, listHeight / 2);
const Offset overList = Offset(400, listHeight / 2);
Future<ScrollController> pumpForwarder(
WidgetTester tester, {
ScrollPosition? Function()? positionProvider,
}) async {
final ScrollController controller = ScrollController();
addTearDown(controller.dispose);
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: ScrollForwarder(
positionProvider:
positionProvider ??
() => controller.hasClients ? controller.position : null,
child: Center(
child: SizedBox(
width: listWidth,
height: listHeight,
child: ListView(
controller: controller,
children: const [
SizedBox(height: 600),
],
),
),
),
),
),
),
);
return controller;
}
Future<void> scrollAt(
WidgetTester tester,
Offset position,
Offset delta,
) async {
final TestPointer pointer = TestPointer(1, PointerDeviceKind.mouse);
pointer.hover(position);
await tester.sendEventToBinding(
PointerScrollEvent(position: position, scrollDelta: delta),
);
}
group("scroll signals", () {
testWidgets("scrolls the target when the pointer is outside the list", (
tester,
) async {
final ScrollController controller = await pumpForwarder(tester);
await scrollAt(tester, margin, const Offset(0, 100));
expect(controller.offset, 100.0);
});
testWidgets("scrolls once when the pointer is over the list", (
tester,
) async {
final ScrollController controller = await pumpForwarder(tester);
await scrollAt(tester, overList, const Offset(0, 100));
expect(controller.offset, 100.0);
await scrollAt(tester, overList, const Offset(0, -40));
expect(controller.offset, 60.0);
});
testWidgets("ignores signals when no position is available", (
tester,
) async {
final ScrollController controller = await pumpForwarder(
tester,
positionProvider: () => null,
);
await scrollAt(tester, margin, const Offset(0, 100));
expect(controller.offset, 0.0);
});
testWidgets("ignores horizontal-only scroll signals", (tester) async {
final ScrollController controller = await pumpForwarder(tester);
await scrollAt(tester, margin, const Offset(50, 0));
expect(controller.offset, 0.0);
});
testWidgets("ignores non-scroll pointer signals", (tester) async {
final ScrollController controller = await pumpForwarder(tester);
await tester.sendEventToBinding(
const PointerScaleEvent(position: margin),
);
expect(controller.offset, 0.0);
});
testWidgets("ignores signals when the position has no content dimensions", (
tester,
) async {
final ScrollController controller = ScrollController();
addTearDown(controller.dispose);
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: ScrollForwarder(
positionProvider: () =>
controller.hasClients ? controller.position : null,
child: Scrollable(
controller: controller,
viewportBuilder: (context, position) => const SizedBox(),
),
),
),
),
);
await scrollAt(tester, margin, const Offset(0, 100));
expect(controller.offset, 0.0);
});
});
group("drags", () {
testWidgets("dragging up on the margin scrolls the target down", (
tester,
) async {
final ScrollController controller = await pumpForwarder(tester);
final TestGesture gesture = await tester.startGesture(margin);
await gesture.moveBy(const Offset(0, 20));
await gesture.moveBy(const Offset(0, -60));
expect(controller.offset, closeTo(60, 10));
await gesture.up();
await tester.pump();
expect(controller.offset, greaterThanOrEqualTo(55));
});
testWidgets("dragging down on the margin scrolls the target up", (
tester,
) async {
final ScrollController controller = await pumpForwarder(tester);
controller.jumpTo(120);
await tester.pump();
final TestGesture gesture = await tester.startGesture(margin);
await gesture.moveBy(const Offset(0, 20));
await gesture.moveBy(const Offset(0, 100));
expect(controller.offset, closeTo(0, 10));
await gesture.up();
await tester.pump();
expect(controller.offset, inInclusiveRange(0, 20));
});
testWidgets("dragging over the list still scrolls the list once", (
tester,
) async {
final ScrollController controller = await pumpForwarder(tester);
final TestGesture gesture = await tester.startGesture(overList);
await gesture.moveBy(const Offset(0, 20));
await gesture.moveBy(const Offset(0, -100));
expect(controller.offset, closeTo(100, 10));
await gesture.up();
await tester.pump();
expect(controller.offset, greaterThanOrEqualTo(95));
});
testWidgets("ignores drags when no position is available", (tester) async {
final ScrollController controller = await pumpForwarder(
tester,
positionProvider: () => null,
);
final TestGesture gesture = await tester.startGesture(margin);
await gesture.moveBy(const Offset(0, 20));
await gesture.moveBy(const Offset(0, -100));
await gesture.up();
await tester.pump();
expect(controller.offset, 0.0);
});
testWidgets("cancels the pending drag when the pointer is released early", (
tester,
) async {
final ScrollController controller = await pumpForwarder(tester);
final TestGesture gesture = await tester.startGesture(margin);
await gesture.up();
await tester.pump();
expect(controller.offset, 0.0);
});
});
group("lifecycle", () {
testWidgets("keeps recognizers when the drag devices are unchanged", (
tester,
) async {
final ScrollController controller = ScrollController();
addTearDown(controller.dispose);
Widget build(ScrollBehavior behavior) {
return MaterialApp(
home: Scaffold(
body: ScrollConfiguration(
behavior: behavior,
child: ScrollForwarder(
positionProvider: () =>
controller.hasClients ? controller.position : null,
child: Center(
child: SizedBox(
width: listWidth,
height: listHeight,
child: ListView(
controller: controller,
children: const [
SizedBox(height: 600),
],
),
),
),
),
),
),
);
}
await tester.pumpWidget(
build(const _FirstDevicesScrollBehavior()),
);
await tester.pumpWidget(
build(const _SecondDevicesScrollBehavior()),
);
final TestGesture gesture = await tester.startGesture(margin);
await gesture.moveBy(const Offset(0, -60));
expect(controller.offset, closeTo(60, 10));
await gesture.up();
await tester.pump();
});
});
}
class _FirstDevicesScrollBehavior extends ScrollBehavior {
const _FirstDevicesScrollBehavior();
@override
Set<PointerDeviceKind> get dragDevices => const {PointerDeviceKind.touch};
}
class _SecondDevicesScrollBehavior extends ScrollBehavior {
const _SecondDevicesScrollBehavior();
@override
Set<PointerDeviceKind> get dragDevices => const {PointerDeviceKind.touch};
}