// 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 'dart:ui' as ui; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; class TestResult { bool dragStarted = false; bool dragUpdate = false; bool dragEnd = false; } class NestedScrollableCase extends StatelessWidget { const NestedScrollableCase({super.key, required this.testResult}); final TestResult testResult; @override Widget build(BuildContext context) { return Scaffold( body: CustomScrollView( slivers: <Widget>[ SliverFixedExtentList( itemExtent: 50.0, delegate: SliverChildBuilderDelegate( (BuildContext context, int index) { return Container( alignment: Alignment.center, child: GestureDetector( behavior: HitTestBehavior.opaque, onVerticalDragDown: (DragDownDetails details) { testResult.dragStarted = true; }, onVerticalDragUpdate: (DragUpdateDetails details){ testResult.dragUpdate = true; }, onVerticalDragEnd: (_) {}, child: Text('List Item $index', key: ValueKey<int>(index), ), ), ); }, ), ), ], ), ); } } class NestedDraggableCase extends StatelessWidget { const NestedDraggableCase({super.key, required this.testResult}); final TestResult testResult; @override Widget build(BuildContext context) { return Scaffold( body: CustomScrollView( slivers: <Widget>[ SliverFixedExtentList( itemExtent: 50.0, delegate: SliverChildBuilderDelegate( (BuildContext context, int index) { return Container( alignment: Alignment.center, child: Draggable<Object>( key: ValueKey<int>(index), feedback: const Text('Dragging'), child: Text('List Item $index'), onDragStarted: () { testResult.dragStarted = true; }, onDragUpdate: (DragUpdateDetails details){ testResult.dragUpdate = true; }, onDragEnd: (_) { testResult.dragEnd = true; }, ), ); }, ), ), ], ), ); } } void main() { testWidgetsWithLeakTracking('Scroll Views get the same ScrollConfiguration as GestureDetectors', (WidgetTester tester) async { tester.view.gestureSettings = const ui.GestureSettings(physicalTouchSlop: 4); addTearDown(tester.view.reset); final TestResult result = TestResult(); await tester.pumpWidget(MaterialApp( title: 'Scroll Bug', home: NestedScrollableCase(testResult: result), )); // By dragging the scroll view more than the configured touch slop above but less than // the framework default value, we demonstrate that this causes gesture detectors // that do not receive the same gesture settings to fire at different times than would // be expected. final Offset start = tester.getCenter(find.byKey(const ValueKey<int>(1))); await tester.timedDragFrom(start, const Offset(0, 5), const Duration(milliseconds: 50)); await tester.pumpAndSettle(); expect(result.dragStarted, true); expect(result.dragUpdate, true); }); testWidgetsWithLeakTracking('Scroll Views get the same ScrollConfiguration as Draggables', (WidgetTester tester) async { tester.view.gestureSettings = const ui.GestureSettings(physicalTouchSlop: 4); addTearDown(tester.view.reset); final TestResult result = TestResult(); await tester.pumpWidget(MaterialApp( title: 'Scroll Bug', home: NestedDraggableCase(testResult: result), )); // By dragging the scroll view more than the configured touch slop above but less than // the framework default value, we demonstrate that this causes gesture detectors // that do not receive the same gesture settings to fire at different times than would // be expected. final Offset start = tester.getCenter(find.byKey(const ValueKey<int>(1))); await tester.timedDragFrom(start, const Offset(0, 5), const Duration(milliseconds: 50)); await tester.pumpAndSettle(); expect(result.dragStarted, true); expect(result.dragUpdate, true); expect(result.dragEnd, true); }); }