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 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 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 get dragDevices => const {PointerDeviceKind.touch}; } class _SecondDevicesScrollBehavior extends ScrollBehavior { const _SecondDevicesScrollBehavior(); @override Set get dragDevices => const {PointerDeviceKind.touch}; }