// 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. // TODO(gspencergoog): Remove this tag once this test's state leaks/test // dependency have been fixed. // https://github.com/flutter/flutter/issues/85160 // Fails with "flutter test --test-randomize-ordering-seed=123" @Tags(<String>['no-shuffle']) import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/semantics.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'semantics_tester.dart'; void main() { testWidgets('Drag and drop - control test', (WidgetTester tester) async { final List<int> accepted = <int>[]; final List<DragTargetDetails<int>> acceptedDetails = <DragTargetDetails<int>>[]; int dragStartedCount = 0; int moveCount = 0; await tester.pumpWidget(MaterialApp( home: Column( children: <Widget>[ Draggable<int>( data: 1, child: const Text('Source'), feedback: const Text('Dragging'), onDragStarted: () { ++dragStartedCount; }, ), DragTarget<int>( builder: (BuildContext context, List<int?> data, List<dynamic> rejects) { return const SizedBox(height: 100.0, child: Text('Target')); }, onMove: (_) => moveCount++, onAccept: accepted.add, onAcceptWithDetails: acceptedDetails.add, ), ], ), )); expect(accepted, isEmpty); expect(acceptedDetails, isEmpty); expect(find.text('Source'), findsOneWidget); expect(find.text('Dragging'), findsNothing); expect(find.text('Target'), findsOneWidget); expect(dragStartedCount, 0); expect(moveCount, 0); final Offset firstLocation = tester.getCenter(find.text('Source')); final TestGesture gesture = await tester.startGesture(firstLocation, pointer: 7); await tester.pump(); expect(accepted, isEmpty); expect(acceptedDetails, isEmpty); expect(find.text('Source'), findsOneWidget); expect(find.text('Dragging'), findsOneWidget); expect(find.text('Target'), findsOneWidget); expect(dragStartedCount, 1); expect(moveCount, 0); final Offset secondLocation = tester.getCenter(find.text('Target')); await gesture.moveTo(secondLocation); await tester.pump(); expect(accepted, isEmpty); expect(acceptedDetails, isEmpty); expect(find.text('Source'), findsOneWidget); expect(find.text('Dragging'), findsOneWidget); expect(find.text('Target'), findsOneWidget); expect(dragStartedCount, 1); expect(moveCount, 1); await gesture.up(); await tester.pump(); expect(accepted, equals(<int>[1])); expect(acceptedDetails, hasLength(1)); expect(acceptedDetails.first.offset, const Offset(256.0, 74.0)); expect(find.text('Source'), findsOneWidget); expect(find.text('Dragging'), findsNothing); expect(find.text('Target'), findsOneWidget); expect(dragStartedCount, 1); expect(moveCount, 1); }); // Regression test for https://github.com/flutter/flutter/issues/76825 testWidgets('Drag and drop - onLeave callback fires correctly with generic parameter', (WidgetTester tester) async { final Map<String,int> leftBehind = <String,int>{ 'Target 1': 0, 'Target 2': 0, }; await tester.pumpWidget(MaterialApp( home: Column( children: <Widget>[ const Draggable<int>( data: 1, child: Text('Source'), feedback: Text('Dragging'), ), DragTarget<int>( builder: (BuildContext context, List<int?> data, List<dynamic> rejects) { return const SizedBox(height: 100.0, child: Text('Target 1')); }, onLeave: (int? data) { if (data != null) { leftBehind['Target 1'] = leftBehind['Target 1']! + data; } }, ), DragTarget<int>( builder: (BuildContext context, List<int?> data, List<dynamic> rejects) { return const SizedBox(height: 100.0, child: Text('Target 2')); }, onLeave: (int? data) { if (data != null) { leftBehind['Target 2'] = leftBehind['Target 2']! + data; } }, ), ], ), )); expect(leftBehind['Target 1'], equals(0)); expect(leftBehind['Target 2'], equals(0)); final Offset firstLocation = tester.getCenter(find.text('Source')); final TestGesture gesture = await tester.startGesture(firstLocation, pointer: 7); await tester.pump(); expect(leftBehind['Target 1'], equals(0)); expect(leftBehind['Target 2'], equals(0)); final Offset secondLocation = tester.getCenter(find.text('Target 1')); await gesture.moveTo(secondLocation); await tester.pump(); expect(leftBehind['Target 1'], equals(0)); expect(leftBehind['Target 2'], equals(0)); final Offset thirdLocation = tester.getCenter(find.text('Target 2')); await gesture.moveTo(thirdLocation); await tester.pump(); expect(leftBehind['Target 1'], equals(1)); expect(leftBehind['Target 2'], equals(0)); await gesture.moveTo(secondLocation); await tester.pump(); expect(leftBehind['Target 1'], equals(1)); expect(leftBehind['Target 2'], equals(1)); await gesture.up(); await tester.pump(); expect(leftBehind['Target 1'], equals(1)); expect(leftBehind['Target 2'], equals(1)); }); testWidgets('Drag and drop - onLeave callback fires correctly', (WidgetTester tester) async { final Map<String,int> leftBehind = <String,int>{ 'Target 1': 0, 'Target 2': 0, }; await tester.pumpWidget(MaterialApp( home: Column( children: <Widget>[ const Draggable<int>( data: 1, child: Text('Source'), feedback: Text('Dragging'), ), DragTarget<int>( builder: (BuildContext context, List<int?> data, List<dynamic> rejects) { return const SizedBox(height: 100.0, child: Text('Target 1')); }, onLeave: (Object? data) { if (data is int) { leftBehind['Target 1'] = leftBehind['Target 1']! + data; } }, ), DragTarget<int>( builder: (BuildContext context, List<int?> data, List<dynamic> rejects) { return const SizedBox(height: 100.0, child: Text('Target 2')); }, onLeave: (Object? data) { if (data is int) { leftBehind['Target 2'] = leftBehind['Target 2']! + data; } }, ), ], ), )); expect(leftBehind['Target 1'], equals(0)); expect(leftBehind['Target 2'], equals(0)); final Offset firstLocation = tester.getCenter(find.text('Source')); final TestGesture gesture = await tester.startGesture(firstLocation, pointer: 7); await tester.pump(); expect(leftBehind['Target 1'], equals(0)); expect(leftBehind['Target 2'], equals(0)); final Offset secondLocation = tester.getCenter(find.text('Target 1')); await gesture.moveTo(secondLocation); await tester.pump(); expect(leftBehind['Target 1'], equals(0)); expect(leftBehind['Target 2'], equals(0)); final Offset thirdLocation = tester.getCenter(find.text('Target 2')); await gesture.moveTo(thirdLocation); await tester.pump(); expect(leftBehind['Target 1'], equals(1)); expect(leftBehind['Target 2'], equals(0)); await gesture.moveTo(secondLocation); await tester.pump(); expect(leftBehind['Target 1'], equals(1)); expect(leftBehind['Target 2'], equals(1)); await gesture.up(); await tester.pump(); expect(leftBehind['Target 1'], equals(1)); expect(leftBehind['Target 2'], equals(1)); }); // Regression test for https://github.com/flutter/flutter/issues/76825 testWidgets('Drag and drop - onMove callback fires correctly with generic parameter', (WidgetTester tester) async { final Map<String,int> targetMoveCount = <String,int>{ 'Target 1': 0, 'Target 2': 0, }; await tester.pumpWidget(MaterialApp( home: Column( children: <Widget>[ const Draggable<int>( data: 1, child: Text('Source'), feedback: Text('Dragging'), ), DragTarget<int>( builder: (BuildContext context, List<int?> data, List<dynamic> rejects) { return const SizedBox(height: 100.0, child: Text('Target 1')); }, onMove: (DragTargetDetails<int> details) { targetMoveCount['Target 1'] = targetMoveCount['Target 1']! + details.data; }, ), DragTarget<int>( builder: (BuildContext context, List<int?> data, List<dynamic> rejects) { return const SizedBox(height: 100.0, child: Text('Target 2')); }, onMove: (DragTargetDetails<int> details) { targetMoveCount['Target 2'] = targetMoveCount['Target 2']! + details.data; }, ), ], ), )); expect(targetMoveCount['Target 1'], equals(0)); expect(targetMoveCount['Target 2'], equals(0)); final Offset firstLocation = tester.getCenter(find.text('Source')); final TestGesture gesture = await tester.startGesture(firstLocation, pointer: 7); await tester.pump(); expect(targetMoveCount['Target 1'], equals(0)); expect(targetMoveCount['Target 2'], equals(0)); final Offset secondLocation = tester.getCenter(find.text('Target 1')); await gesture.moveTo(secondLocation); await tester.pump(); expect(targetMoveCount['Target 1'], equals(1)); expect(targetMoveCount['Target 2'], equals(0)); final Offset thirdLocation = tester.getCenter(find.text('Target 2')); await gesture.moveTo(thirdLocation); await tester.pump(); expect(targetMoveCount['Target 1'], equals(1)); expect(targetMoveCount['Target 2'], equals(1)); await gesture.moveTo(secondLocation); await tester.pump(); expect(targetMoveCount['Target 1'], equals(2)); expect(targetMoveCount['Target 2'], equals(1)); await gesture.up(); await tester.pump(); expect(targetMoveCount['Target 1'], equals(2)); expect(targetMoveCount['Target 2'], equals(1)); }); testWidgets('Drag and drop - onMove callback fires correctly', (WidgetTester tester) async { final Map<String,int> targetMoveCount = <String,int>{ 'Target 1': 0, 'Target 2': 0, }; await tester.pumpWidget(MaterialApp( home: Column( children: <Widget>[ const Draggable<int>( data: 1, child: Text('Source'), feedback: Text('Dragging'), ), DragTarget<int>( builder: (BuildContext context, List<int?> data, List<dynamic> rejects) { return const SizedBox(height: 100.0, child: Text('Target 1')); }, onMove: (DragTargetDetails<dynamic> details) { if (details.data is int) { targetMoveCount['Target 1'] = targetMoveCount['Target 1']! + (details.data as int); } }, ), DragTarget<int>( builder: (BuildContext context, List<int?> data, List<dynamic> rejects) { return const SizedBox(height: 100.0, child: Text('Target 2')); }, onMove: (DragTargetDetails<dynamic> details) { if (details.data is int) { targetMoveCount['Target 2'] = targetMoveCount['Target 2']! + (details.data as int); } }, ), ], ), )); expect(targetMoveCount['Target 1'], equals(0)); expect(targetMoveCount['Target 2'], equals(0)); final Offset firstLocation = tester.getCenter(find.text('Source')); final TestGesture gesture = await tester.startGesture(firstLocation, pointer: 7); await tester.pump(); expect(targetMoveCount['Target 1'], equals(0)); expect(targetMoveCount['Target 2'], equals(0)); final Offset secondLocation = tester.getCenter(find.text('Target 1')); await gesture.moveTo(secondLocation); await tester.pump(); expect(targetMoveCount['Target 1'], equals(1)); expect(targetMoveCount['Target 2'], equals(0)); final Offset thirdLocation = tester.getCenter(find.text('Target 2')); await gesture.moveTo(thirdLocation); await tester.pump(); expect(targetMoveCount['Target 1'], equals(1)); expect(targetMoveCount['Target 2'], equals(1)); await gesture.moveTo(secondLocation); await tester.pump(); expect(targetMoveCount['Target 1'], equals(2)); expect(targetMoveCount['Target 2'], equals(1)); await gesture.up(); await tester.pump(); expect(targetMoveCount['Target 1'], equals(2)); expect(targetMoveCount['Target 2'], equals(1)); }); testWidgets('Drag and drop - dragging over button', (WidgetTester tester) async { final List<String> events = <String>[]; Offset firstLocation, secondLocation; await tester.pumpWidget(MaterialApp( home: Column( children: <Widget>[ const Draggable<int>( data: 1, child: Text('Source'), feedback: Text('Dragging'), ), Stack( children: <Widget>[ GestureDetector( behavior: HitTestBehavior.opaque, onTap: () { events.add('tap'); }, child: const Text('Button'), ), DragTarget<int>( builder: (BuildContext context, List<int?> data, List<dynamic> rejects) { return const IgnorePointer( child: Text('Target'), ); }, onAccept: (int? data) { events.add('drop'); }, onAcceptWithDetails: (DragTargetDetails<int> _) { events.add('details'); }, ), ], ), ], ), )); expect(events, isEmpty); expect(find.text('Source'), findsOneWidget); expect(find.text('Dragging'), findsNothing); expect(find.text('Target'), findsOneWidget); expect(find.text('Button'), findsOneWidget); // taps (we check both to make sure the test is consistent) expect(events, isEmpty); await tester.tap(find.text('Button')); expect(events, equals(<String>['tap'])); events.clear(); expect(events, isEmpty); await tester.tap(find.text('Target'), warnIfMissed: false); // (inside IgnorePointer) expect(events, equals(<String>['tap'])); events.clear(); // drag and drop firstLocation = tester.getCenter(find.text('Source')); TestGesture gesture = await tester.startGesture(firstLocation, pointer: 7); await tester.pump(); secondLocation = tester.getCenter(find.text('Target')); await gesture.moveTo(secondLocation); await tester.pump(); expect(events, isEmpty); await gesture.up(); await tester.pump(); expect(events, equals(<String>['drop', 'details'])); events.clear(); // drag and tap and drop firstLocation = tester.getCenter(find.text('Source')); gesture = await tester.startGesture(firstLocation, pointer: 7); await tester.pump(); secondLocation = tester.getCenter(find.text('Target')); await gesture.moveTo(secondLocation); await tester.pump(); expect(events, isEmpty); await tester.tap(find.text('Button')); await tester.tap(find.text('Target'), warnIfMissed: false); // (inside IgnorePointer) await gesture.up(); await tester.pump(); expect(events, equals(<String>['tap', 'tap', 'drop', 'details'])); events.clear(); }); testWidgets('Drag and drop - tapping button', (WidgetTester tester) async { final List<String> events = <String>[]; Offset firstLocation, secondLocation; await tester.pumpWidget(MaterialApp( home: Column( children: <Widget>[ Draggable<int>( data: 1, child: GestureDetector( behavior: HitTestBehavior.opaque, onTap: () { events.add('tap'); }, child: const Text('Button'), ), feedback: const Text('Dragging'), ), DragTarget<int>( builder: (BuildContext context, List<int?> data, List<dynamic> rejects) { return const Text('Target'); }, onAccept: (int? data) { events.add('drop'); }, onAcceptWithDetails: (DragTargetDetails<int> _) { events.add('details'); }, ), ], ), )); expect(events, isEmpty); expect(find.text('Button'), findsOneWidget); expect(find.text('Target'), findsOneWidget); expect(events, isEmpty); await tester.tap(find.text('Button')); expect(events, equals(<String>['tap'])); events.clear(); firstLocation = tester.getCenter(find.text('Button')); final TestGesture gesture = await tester.startGesture(firstLocation, pointer: 7); await tester.pump(); secondLocation = tester.getCenter(find.text('Target')); await gesture.moveTo(secondLocation); await tester.pump(); expect(events, isEmpty); await gesture.up(); await tester.pump(); expect(events, equals(<String>['drop', 'details'])); events.clear(); }); testWidgets('Drag and drop - long press draggable, short press', (WidgetTester tester) async { final List<String> events = <String>[]; Offset firstLocation, secondLocation; await tester.pumpWidget(MaterialApp( home: Column( children: <Widget>[ const LongPressDraggable<int>( data: 1, child: Text('Source'), feedback: Text('Dragging'), ), DragTarget<int>( builder: (BuildContext context, List<int?> data, List<dynamic> rejects) { return const Text('Target'); }, onAccept: (int? data) { events.add('drop'); }, onAcceptWithDetails: (DragTargetDetails<int> _) { events.add('details'); }, ), ], ), )); expect(events, isEmpty); expect(find.text('Source'), findsOneWidget); expect(find.text('Target'), findsOneWidget); expect(events, isEmpty); await tester.tap(find.text('Source')); expect(events, isEmpty); firstLocation = tester.getCenter(find.text('Source')); final TestGesture gesture = await tester.startGesture(firstLocation, pointer: 7); await tester.pump(); secondLocation = tester.getCenter(find.text('Target')); await gesture.moveTo(secondLocation); await tester.pump(); expect(events, isEmpty); await gesture.up(); await tester.pump(); expect(events, isEmpty); }); testWidgets('Drag and drop - long press draggable, long press', (WidgetTester tester) async { final List<String> events = <String>[]; Offset firstLocation, secondLocation; await tester.pumpWidget(MaterialApp( home: Column( children: <Widget>[ const Draggable<int>( data: 1, child: Text('Source'), feedback: Text('Dragging'), ), DragTarget<int>( builder: (BuildContext context, List<int?> data, List<dynamic> rejects) { return const Text('Target'); }, onAccept: (int? data) { events.add('drop'); }, onAcceptWithDetails: (DragTargetDetails<int> _) { events.add('details'); }, ), ], ), )); expect(events, isEmpty); expect(find.text('Source'), findsOneWidget); expect(find.text('Target'), findsOneWidget); expect(events, isEmpty); await tester.tap(find.text('Source')); expect(events, isEmpty); firstLocation = tester.getCenter(find.text('Source')); final TestGesture gesture = await tester.startGesture(firstLocation, pointer: 7); await tester.pump(); await tester.pump(const Duration(seconds: 20)); secondLocation = tester.getCenter(find.text('Target')); await gesture.moveTo(secondLocation); await tester.pump(); expect(events, isEmpty); await gesture.up(); await tester.pump(); expect(events, equals(<String>['drop', 'details'])); }); testWidgets('Drag and drop - horizontal and vertical draggables in vertical block', (WidgetTester tester) async { final List<String> events = <String>[]; Offset firstLocation, secondLocation, thirdLocation; await tester.pumpWidget(MaterialApp( home: ListView( dragStartBehavior: DragStartBehavior.down, children: <Widget>[ DragTarget<int>( builder: (BuildContext context, List<int?> data, List<dynamic> rejects) { return const Text('Target'); }, onAccept: (int? data) { events.add('drop $data'); }, onAcceptWithDetails: (DragTargetDetails<int> _) { events.add('details'); }, ), Container(height: 400.0), const Draggable<int>( data: 1, child: Text('H'), feedback: Text('Dragging'), affinity: Axis.horizontal, ), const Draggable<int>( data: 2, child: Text('V'), feedback: Text('Dragging'), affinity: Axis.vertical, ), Container(height: 500.0), Container(height: 500.0), Container(height: 500.0), Container(height: 500.0), ], ), )); expect(events, isEmpty); expect(find.text('Target'), findsOneWidget); expect(find.text('H'), findsOneWidget); expect(find.text('V'), findsOneWidget); // vertical draggable drags vertically expect(events, isEmpty); firstLocation = tester.getCenter(find.text('V')); secondLocation = tester.getCenter(find.text('Target')); TestGesture gesture = await tester.startGesture(firstLocation, pointer: 7); await tester.pump(); await gesture.moveTo(secondLocation); await tester.pump(); await gesture.up(); await tester.pump(); expect(events, equals(<String>['drop 2', 'details'])); expect(tester.getCenter(find.text('Target')).dy, greaterThan(0.0)); events.clear(); // horizontal draggable drags horizontally expect(events, isEmpty); firstLocation = tester.getTopLeft(find.text('H')); secondLocation = tester.getTopRight(find.text('H')); thirdLocation = tester.getCenter(find.text('Target')); gesture = await tester.startGesture(firstLocation, pointer: 7); await tester.pump(); await gesture.moveTo(secondLocation); await tester.pump(); await gesture.moveTo(thirdLocation); await tester.pump(); await gesture.up(); await tester.pump(); expect(events, equals(<String>['drop 1', 'details'])); expect(tester.getCenter(find.text('Target')).dy, greaterThan(0.0)); events.clear(); // vertical draggable drags horizontally when there's no competition // from other gesture detectors expect(events, isEmpty); firstLocation = tester.getTopLeft(find.text('V')); secondLocation = tester.getTopRight(find.text('V')); thirdLocation = tester.getCenter(find.text('Target')); gesture = await tester.startGesture(firstLocation, pointer: 7); await tester.pump(); await gesture.moveTo(secondLocation); await tester.pump(); await gesture.moveTo(thirdLocation); await tester.pump(); await gesture.up(); await tester.pump(); expect(events, equals(<String>['drop 2', 'details'])); expect(tester.getCenter(find.text('Target')).dy, greaterThan(0.0)); events.clear(); // horizontal draggable doesn't drag vertically when there is competition // for vertical gestures expect(events, isEmpty); firstLocation = tester.getCenter(find.text('H')); secondLocation = tester.getCenter(find.text('Target')); gesture = await tester.startGesture(firstLocation, pointer: 7); await tester.pump(); await gesture.moveTo(secondLocation); await tester.pump(); // scrolls off screen! await gesture.up(); await tester.pump(); expect(events, equals(<String>[])); expect(find.text('Target'), findsNothing); events.clear(); }); testWidgets('Drag and drop - horizontal and vertical draggables in horizontal block', (WidgetTester tester) async { final List<String> events = <String>[]; Offset firstLocation, secondLocation, thirdLocation; await tester.pumpWidget(MaterialApp( home: ListView( dragStartBehavior: DragStartBehavior.down, scrollDirection: Axis.horizontal, children: <Widget>[ DragTarget<int>( builder: (BuildContext context, List<int?> data, List<dynamic> rejects) { return const Text('Target'); }, onAccept: (int? data) { events.add('drop $data'); }, onAcceptWithDetails: (DragTargetDetails<int> _) { events.add('details'); }, ), Container(width: 400.0), const Draggable<int>( data: 1, child: Text('H'), feedback: Text('Dragging'), affinity: Axis.horizontal, ), const Draggable<int>( data: 2, child: Text('V'), feedback: Text('Dragging'), affinity: Axis.vertical, ), Container(width: 500.0), Container(width: 500.0), Container(width: 500.0), Container(width: 500.0), ], ), )); expect(events, isEmpty); expect(find.text('Target'), findsOneWidget); expect(find.text('H'), findsOneWidget); expect(find.text('V'), findsOneWidget); // horizontal draggable drags horizontally expect(events, isEmpty); firstLocation = tester.getCenter(find.text('H')); secondLocation = tester.getCenter(find.text('Target')); TestGesture gesture = await tester.startGesture(firstLocation, pointer: 7); await tester.pump(); await gesture.moveTo(secondLocation); await tester.pump(); await gesture.up(); await tester.pump(); expect(events, equals(<String>['drop 1', 'details'])); expect(tester.getCenter(find.text('Target')).dx, greaterThan(0.0)); events.clear(); // vertical draggable drags vertically expect(events, isEmpty); firstLocation = tester.getTopLeft(find.text('V')); secondLocation = tester.getBottomLeft(find.text('V')); thirdLocation = tester.getCenter(find.text('Target')); gesture = await tester.startGesture(firstLocation, pointer: 7); await tester.pump(); await gesture.moveTo(secondLocation); await tester.pump(); await gesture.moveTo(thirdLocation); await tester.pump(); await gesture.up(); await tester.pump(); expect(events, equals(<String>['drop 2', 'details'])); expect(tester.getCenter(find.text('Target')).dx, greaterThan(0.0)); events.clear(); // horizontal draggable drags vertically when there's no competition // from other gesture detectors expect(events, isEmpty); firstLocation = tester.getTopLeft(find.text('H')); secondLocation = tester.getBottomLeft(find.text('H')); thirdLocation = tester.getCenter(find.text('Target')); gesture = await tester.startGesture(firstLocation, pointer: 7); await tester.pump(); await gesture.moveTo(secondLocation); await tester.pump(); await gesture.moveTo(thirdLocation); await tester.pump(); await gesture.up(); await tester.pump(); expect(events, equals(<String>['drop 1', 'details'])); expect(tester.getCenter(find.text('Target')).dx, greaterThan(0.0)); events.clear(); // vertical draggable doesn't drag horizontally when there is competition // for horizontal gestures expect(events, isEmpty); firstLocation = tester.getCenter(find.text('V')); secondLocation = tester.getCenter(find.text('Target')); gesture = await tester.startGesture(firstLocation, pointer: 7); await tester.pump(); await gesture.moveTo(secondLocation); await tester.pump(); // scrolls off screen! await gesture.up(); await tester.pump(); expect(events, equals(<String>[])); expect(find.text('Target'), findsNothing); events.clear(); }); group('Drag and drop - Draggables with a set axis only move along that axis', () { final List<String> events = <String>[]; Widget build() { return MaterialApp( home: ListView( scrollDirection: Axis.horizontal, children: <Widget>[ DragTarget<int>( builder: (BuildContext context, List<int?> data, List<dynamic> rejects) { return const Text('Target'); }, onAccept: (int? data) { events.add('drop $data'); }, onAcceptWithDetails: (DragTargetDetails<int> _) { events.add('details'); }, ), Container(width: 400.0), const Draggable<int>( data: 1, child: Text('H'), feedback: Text('H'), childWhenDragging: SizedBox(), axis: Axis.horizontal, ), const Draggable<int>( data: 2, child: Text('V'), feedback: Text('V'), childWhenDragging: SizedBox(), axis: Axis.vertical, ), const Draggable<int>( data: 3, child: Text('N'), feedback: Text('N'), childWhenDragging: SizedBox(), ), Container(width: 500.0), Container(width: 500.0), Container(width: 500.0), Container(width: 500.0), ], ), ); } testWidgets('Null axis draggable moves along all axes', (WidgetTester tester) async { await tester.pumpWidget(build()); final Offset firstLocation = tester.getTopLeft(find.text('N')); final Offset secondLocation = firstLocation + const Offset(300.0, 300.0); final Offset thirdLocation = firstLocation + const Offset(-300.0, -300.0); final TestGesture gesture = await tester.startGesture(firstLocation, pointer: 7); await tester.pump(); await gesture.moveTo(secondLocation); await tester.pump(); expect(tester.getTopLeft(find.text('N')), secondLocation); await gesture.moveTo(thirdLocation); await tester.pump(); expect(tester.getTopLeft(find.text('N')), thirdLocation); }); testWidgets('Horizontal axis draggable moves horizontally', (WidgetTester tester) async { await tester.pumpWidget(build()); final Offset firstLocation = tester.getTopLeft(find.text('H')); final Offset secondLocation = firstLocation + const Offset(300.0, 0.0); final Offset thirdLocation = firstLocation + const Offset(-300.0, 0.0); final TestGesture gesture = await tester.startGesture(firstLocation, pointer: 7); await tester.pump(); await gesture.moveTo(secondLocation); await tester.pump(); expect(tester.getTopLeft(find.text('H')), secondLocation); await gesture.moveTo(thirdLocation); await tester.pump(); expect(tester.getTopLeft(find.text('H')), thirdLocation); }); testWidgets('Horizontal axis draggable does not move vertically', (WidgetTester tester) async { await tester.pumpWidget(build()); final Offset firstLocation = tester.getTopLeft(find.text('H')); final Offset secondDragLocation = firstLocation + const Offset(300.0, 200.0); // The horizontal drag widget won't scroll vertically. final Offset secondWidgetLocation = firstLocation + const Offset(300.0, 0.0); final Offset thirdDragLocation = firstLocation + const Offset(-300.0, -200.0); final Offset thirdWidgetLocation = firstLocation + const Offset(-300.0, 0.0); final TestGesture gesture = await tester.startGesture(firstLocation, pointer: 7); await tester.pump(); await gesture.moveTo(secondDragLocation); await tester.pump(); expect(tester.getTopLeft(find.text('H')), secondWidgetLocation); await gesture.moveTo(thirdDragLocation); await tester.pump(); expect(tester.getTopLeft(find.text('H')), thirdWidgetLocation); }); testWidgets('Vertical axis draggable moves vertically', (WidgetTester tester) async { await tester.pumpWidget(build()); final Offset firstLocation = tester.getTopLeft(find.text('V')); final Offset secondLocation = firstLocation + const Offset(0.0, 300.0); final Offset thirdLocation = firstLocation + const Offset(0.0, -300.0); final TestGesture gesture = await tester.startGesture(firstLocation, pointer: 7); await tester.pump(); await gesture.moveTo(secondLocation); await tester.pump(); expect(tester.getTopLeft(find.text('V')), secondLocation); await gesture.moveTo(thirdLocation); await tester.pump(); expect(tester.getTopLeft(find.text('V')), thirdLocation); }); testWidgets('Vertical axis draggable does not move horizontally', (WidgetTester tester) async { await tester.pumpWidget(build()); final Offset firstLocation = tester.getTopLeft(find.text('V')); final Offset secondDragLocation = firstLocation + const Offset(200.0, 300.0); // The vertical drag widget won't scroll horizontally. final Offset secondWidgetLocation = firstLocation + const Offset(0.0, 300.0); final Offset thirdDragLocation = firstLocation + const Offset(-200.0, -300.0); final Offset thirdWidgetLocation = firstLocation + const Offset(0.0, -300.0); final TestGesture gesture = await tester.startGesture(firstLocation, pointer: 7); await tester.pump(); await gesture.moveTo(secondDragLocation); await tester.pump(); expect(tester.getTopLeft(find.text('V')), secondWidgetLocation); await gesture.moveTo(thirdDragLocation); await tester.pump(); expect(tester.getTopLeft(find.text('V')), thirdWidgetLocation); }); }); group('Drag and drop - onDragUpdate called if draggable moves along a set axis', () { int updated = 0; Offset dragDelta = Offset.zero; setUp(() { updated = 0; dragDelta = Offset.zero; }); Widget build() { return MaterialApp( home: Column( children: <Widget>[ Draggable<int>( data: 1, child: const Text('Source'), feedback: const Text('Dragging'), onDragUpdate: (DragUpdateDetails details) { dragDelta += details.delta; updated++; }, ), Draggable<int>( data: 2, child: const Text('Vertical Source'), feedback: const Text('Vertical Dragging'), onDragUpdate: (DragUpdateDetails details) { dragDelta += details.delta; updated++; }, axis: Axis.vertical, ), Draggable<int>( data: 3, child: const Text('Horizontal Source'), feedback: const Text('Horizontal Dragging'), onDragUpdate: (DragUpdateDetails details) { dragDelta += details.delta; updated++; }, axis: Axis.horizontal, ), ], ), ); } testWidgets('Null axis onDragUpdate called only if draggable moves in any direction', (WidgetTester tester) async { await tester.pumpWidget(build()); expect(updated, 0); expect(find.text('Source'), findsOneWidget); expect(find.text('Dragging'), findsNothing); final Offset firstLocation = tester.getCenter(find.text('Source')); final TestGesture gesture = await tester.startGesture(firstLocation, pointer: 7); await tester.pump(); expect(updated, 0); expect(find.text('Source'), findsOneWidget); expect(find.text('Dragging'), findsOneWidget); await gesture.moveBy(const Offset(10, 10)); await tester.pump(); expect(updated, 1); await gesture.moveBy(Offset.zero); await tester.pump(); expect(updated, 1); await gesture.up(); await tester.pump(); expect(updated, 1); expect(find.text('Source'), findsOneWidget); expect(find.text('Dragging'), findsNothing); expect(dragDelta.dx, 10); expect(dragDelta.dy, 10); }); testWidgets('Vertical axis onDragUpdate only called if draggable moves vertical', (WidgetTester tester) async { await tester.pumpWidget(build()); expect(updated, 0); expect(find.text('Vertical Source'), findsOneWidget); expect(find.text('Vertical Dragging'), findsNothing); final Offset firstLocation = tester.getCenter(find.text('Vertical Source')); final TestGesture gesture = await tester.startGesture(firstLocation, pointer: 7); await tester.pump(); expect(updated, 0); expect(find.text('Vertical Source'), findsOneWidget); expect(find.text('Vertical Dragging'), findsOneWidget); await gesture.moveBy(const Offset(0, 10)); await tester.pump(); expect(updated, 1); await gesture.moveBy(const Offset(10 , 0)); await tester.pump(); expect(updated, 1); await gesture.up(); await tester.pump(); expect(updated, 1); expect(find.text('Vertical Source'), findsOneWidget); expect(find.text('Vertical Dragging'), findsNothing); expect(dragDelta.dx, 0); expect(dragDelta.dy, 10); }); testWidgets('Horizontal axis onDragUpdate only called if draggable moves horizontal', (WidgetTester tester) async { await tester.pumpWidget(build()); expect(updated, 0); expect(find.text('Horizontal Source'), findsOneWidget); expect(find.text('Horizontal Dragging'), findsNothing); final Offset firstLocation = tester.getCenter(find.text('Horizontal Source')); final TestGesture gesture = await tester.startGesture(firstLocation, pointer: 7); await tester.pump(); expect(updated, 0); expect(find.text('Horizontal Source'), findsOneWidget); expect(find.text('Horizontal Dragging'), findsOneWidget); await gesture.moveBy(const Offset(0, 10)); await tester.pump(); expect(updated, 0); await gesture.moveBy(const Offset(10 , 0)); await tester.pump(); expect(updated, 1); await gesture.up(); await tester.pump(); expect(updated, 1); expect(find.text('Horizontal Source'), findsOneWidget); expect(find.text('Horizontal Dragging'), findsNothing); expect(dragDelta.dx, 10); expect(dragDelta.dy, 0); }); }); testWidgets('Drag and drop - onDraggableCanceled not called if dropped on accepting target', (WidgetTester tester) async { final List<int> accepted = <int>[]; final List<DragTargetDetails<int>> acceptedDetails = <DragTargetDetails<int>>[]; bool onDraggableCanceledCalled = false; await tester.pumpWidget(MaterialApp( home: Column( children: <Widget>[ Draggable<int>( data: 1, child: const Text('Source'), feedback: const Text('Dragging'), onDraggableCanceled: (Velocity velocity, Offset offset) { onDraggableCanceledCalled = true; }, ), DragTarget<int>( builder: (BuildContext context, List<int?> data, List<dynamic> rejects) { return const SizedBox(height: 100.0, child: Text('Target')); }, onAccept: accepted.add, onAcceptWithDetails: acceptedDetails.add, ), ], ), )); expect(accepted, isEmpty); expect(acceptedDetails, isEmpty); expect(find.text('Source'), findsOneWidget); expect(find.text('Dragging'), findsNothing); expect(find.text('Target'), findsOneWidget); expect(onDraggableCanceledCalled, isFalse); final Offset firstLocation = tester.getCenter(find.text('Source')); final TestGesture gesture = await tester.startGesture(firstLocation, pointer: 7); await tester.pump(); expect(accepted, isEmpty); expect(acceptedDetails, isEmpty); expect(find.text('Source'), findsOneWidget); expect(find.text('Dragging'), findsOneWidget); expect(find.text('Target'), findsOneWidget); expect(onDraggableCanceledCalled, isFalse); final Offset secondLocation = tester.getCenter(find.text('Target')); await gesture.moveTo(secondLocation); await tester.pump(); expect(accepted, isEmpty); expect(acceptedDetails, isEmpty); expect(find.text('Source'), findsOneWidget); expect(find.text('Dragging'), findsOneWidget); expect(find.text('Target'), findsOneWidget); expect(onDraggableCanceledCalled, isFalse); await gesture.up(); await tester.pump(); expect(accepted, equals(<int>[1])); expect(acceptedDetails, hasLength(1)); expect(acceptedDetails.first.offset, const Offset(256.0, 74.0)); expect(find.text('Source'), findsOneWidget); expect(find.text('Dragging'), findsNothing); expect(find.text('Target'), findsOneWidget); expect(onDraggableCanceledCalled, isFalse); }); testWidgets('Drag and drop - onDraggableCanceled called if dropped on non-accepting target', (WidgetTester tester) async { final List<int> accepted = <int>[]; final List<DragTargetDetails<int>> acceptedDetails = <DragTargetDetails<int>>[]; bool onDraggableCanceledCalled = false; late Velocity onDraggableCanceledVelocity; late Offset onDraggableCanceledOffset; await tester.pumpWidget(MaterialApp( home: Column( children: <Widget>[ Draggable<int>( data: 1, child: const Text('Source'), feedback: const Text('Dragging'), onDraggableCanceled: (Velocity velocity, Offset offset) { onDraggableCanceledCalled = true; onDraggableCanceledVelocity = velocity; onDraggableCanceledOffset = offset; }, ), DragTarget<int>( builder: (BuildContext context, List<int?> data, List<dynamic> rejects) { return const SizedBox( height: 100.0, child: Text('Target'), ); }, onWillAccept: (int? data) => false, onAccept: accepted.add, onAcceptWithDetails: acceptedDetails.add, ), ], ), )); expect(accepted, isEmpty); expect(acceptedDetails, isEmpty); expect(find.text('Source'), findsOneWidget); expect(find.text('Dragging'), findsNothing); expect(find.text('Target'), findsOneWidget); expect(onDraggableCanceledCalled, isFalse); final Offset firstLocation = tester.getTopLeft(find.text('Source')); final TestGesture gesture = await tester.startGesture(firstLocation, pointer: 7); await tester.pump(); expect(accepted, isEmpty); expect(acceptedDetails, isEmpty); expect(find.text('Source'), findsOneWidget); expect(find.text('Dragging'), findsOneWidget); expect(find.text('Target'), findsOneWidget); expect(onDraggableCanceledCalled, isFalse); final Offset secondLocation = tester.getCenter(find.text('Target')); await gesture.moveTo(secondLocation); await tester.pump(); expect(accepted, isEmpty); expect(acceptedDetails, isEmpty); expect(find.text('Source'), findsOneWidget); expect(find.text('Dragging'), findsOneWidget); expect(find.text('Target'), findsOneWidget); expect(onDraggableCanceledCalled, isFalse); await gesture.up(); await tester.pump(); expect(accepted, isEmpty); expect(acceptedDetails, isEmpty); expect(find.text('Source'), findsOneWidget); expect(find.text('Dragging'), findsNothing); expect(find.text('Target'), findsOneWidget); expect(onDraggableCanceledCalled, isTrue); expect(onDraggableCanceledVelocity, equals(Velocity.zero)); expect(onDraggableCanceledOffset, equals(Offset(secondLocation.dx, secondLocation.dy))); }); testWidgets('Drag and drop - onDraggableCanceled called if dropped on non-accepting target with correct velocity', (WidgetTester tester) async { final List<int> accepted = <int>[]; final List<DragTargetDetails<int>> acceptedDetails = <DragTargetDetails<int>>[]; bool onDraggableCanceledCalled = false; late Velocity onDraggableCanceledVelocity; late Offset onDraggableCanceledOffset; await tester.pumpWidget(MaterialApp( home: Column(children: <Widget>[ Draggable<int>( data: 1, child: const Text('Source'), feedback: const Text('Source'), onDraggableCanceled: (Velocity velocity, Offset offset) { onDraggableCanceledCalled = true; onDraggableCanceledVelocity = velocity; onDraggableCanceledOffset = offset; }, ), DragTarget<int>( builder: (BuildContext context, List<int?> data, List<dynamic> rejects) { return const SizedBox(height: 100.0, child: Text('Target')); }, onWillAccept: (int? data) => false, onAccept: accepted.add, onAcceptWithDetails: acceptedDetails.add, ), ]), )); expect(accepted, isEmpty); expect(acceptedDetails, isEmpty); expect(find.text('Source'), findsOneWidget); expect(find.text('Dragging'), findsNothing); expect(find.text('Target'), findsOneWidget); expect(onDraggableCanceledCalled, isFalse); final Offset flingStart = tester.getTopLeft(find.text('Source')); await tester.flingFrom(flingStart, const Offset(0.0, 100.0), 1000.0); await tester.pump(); expect(accepted, isEmpty); expect(acceptedDetails, isEmpty); expect(find.text('Source'), findsOneWidget); expect(find.text('Dragging'), findsNothing); expect(find.text('Target'), findsOneWidget); expect(onDraggableCanceledCalled, isTrue); expect(onDraggableCanceledVelocity.pixelsPerSecond.dx.abs(), lessThan(0.0000001)); expect((onDraggableCanceledVelocity.pixelsPerSecond.dy - 1000.0).abs(), lessThan(0.0000001)); expect(onDraggableCanceledOffset, equals(Offset(flingStart.dx, flingStart.dy) + const Offset(0.0, 100.0))); }); testWidgets('Drag and drop - onDragEnd not called if dropped on non-accepting target', (WidgetTester tester) async { final List<int> accepted = <int>[]; final List<DragTargetDetails<int>> acceptedDetails = <DragTargetDetails<int>>[]; bool onDragEndCalled = false; late DraggableDetails onDragEndDraggableDetails; await tester.pumpWidget(MaterialApp( home: Column( children: <Widget>[ Draggable<int>( data: 1, child: const Text('Source'), feedback: const Text('Dragging'), onDragEnd: (DraggableDetails details) { onDragEndCalled = true; onDragEndDraggableDetails = details; }, ), DragTarget<int>( builder: (BuildContext context, List<int?> data, List<dynamic> rejects) { return const SizedBox(height: 100.0, child: Text('Target')); }, onWillAccept: (int? data) => false, onAccept: accepted.add, onAcceptWithDetails: acceptedDetails.add, ), ], ), )); expect(accepted, isEmpty); expect(acceptedDetails, isEmpty); expect(find.text('Source'), findsOneWidget); expect(find.text('Dragging'), findsNothing); expect(find.text('Target'), findsOneWidget); expect(onDragEndCalled, isFalse); final Offset firstLocation = tester.getTopLeft(find.text('Source')); final TestGesture gesture = await tester.startGesture(firstLocation, pointer: 7); await tester.pump(); expect(accepted, isEmpty); expect(acceptedDetails, isEmpty); expect(find.text('Source'), findsOneWidget); expect(find.text('Dragging'), findsOneWidget); expect(find.text('Target'), findsOneWidget); expect(onDragEndCalled, isFalse); final Offset secondLocation = tester.getCenter(find.text('Target')); await gesture.moveTo(secondLocation); await tester.pump(); expect(accepted, isEmpty); expect(acceptedDetails, isEmpty); expect(find.text('Source'), findsOneWidget); expect(find.text('Dragging'), findsOneWidget); expect(find.text('Target'), findsOneWidget); expect(onDragEndCalled, isFalse); await gesture.up(); await tester.pump(); expect(accepted, isEmpty); expect(acceptedDetails, isEmpty); expect(find.text('Source'), findsOneWidget); expect(find.text('Dragging'), findsNothing); expect(find.text('Target'), findsOneWidget); expect(onDragEndCalled, isTrue); expect(onDragEndDraggableDetails, isNotNull); expect(onDragEndDraggableDetails.wasAccepted, isFalse); expect(onDragEndDraggableDetails.velocity, equals(Velocity.zero)); expect( onDragEndDraggableDetails.offset, equals(Offset(secondLocation.dx, secondLocation.dy - firstLocation.dy)), ); }); testWidgets('Drag and drop - DragTarget rebuilds with and without rejected data when a rejected draggable enters and leaves', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( home: Column( children: <Widget>[ const Draggable<int>( data: 1, child: Text('Source'), feedback: Text('Dragging'), ), DragTarget<int>( builder: (BuildContext context, List<int?> data, List<dynamic> rejects) { return SizedBox( height: 100.0, child: rejects.isNotEmpty ? const Text('Rejected') : const Text('Target'), ); }, onWillAccept: (int? data) => false, ), ], ), )); expect(find.text('Dragging'), findsNothing); expect(find.text('Target'), findsOneWidget); expect(find.text('Rejected'), findsNothing); final Offset firstLocation = tester.getTopLeft(find.text('Source')); final TestGesture gesture = await tester.startGesture(firstLocation, pointer: 7); await tester.pump(); expect(find.text('Dragging'), findsOneWidget); expect(find.text('Target'), findsOneWidget); expect(find.text('Rejected'), findsNothing); final Offset secondLocation = tester.getCenter(find.text('Target')); await gesture.moveTo(secondLocation); await tester.pump(); expect(find.text('Dragging'), findsOneWidget); expect(find.text('Target'), findsNothing); expect(find.text('Rejected'), findsOneWidget); await gesture.moveTo(firstLocation); await tester.pump(); expect(find.text('Dragging'), findsOneWidget); expect(find.text('Target'), findsOneWidget); expect(find.text('Rejected'), findsNothing); }); testWidgets('Drag and drop - Can drag and drop over a non-accepting target multiple times', (WidgetTester tester) async { int numberOfTimesOnDraggableCanceledCalled = 0; await tester.pumpWidget(MaterialApp( home: Column( children: <Widget>[ Draggable<int>( data: 1, child: const Text('Source'), feedback: const Text('Dragging'), onDraggableCanceled: (Velocity velocity, Offset offset) { numberOfTimesOnDraggableCanceledCalled++; }, ), DragTarget<int>( builder: (BuildContext context, List<int?> data, List<dynamic> rejects) { return SizedBox( height: 100.0, child: rejects.isNotEmpty ? const Text('Rejected') : const Text('Target'), ); }, onWillAccept: (int? data) => false, ), ], ), )); expect(find.text('Dragging'), findsNothing); expect(find.text('Target'), findsOneWidget); expect(find.text('Rejected'), findsNothing); final Offset firstLocation = tester.getTopLeft(find.text('Source')); final TestGesture gesture = await tester.startGesture(firstLocation, pointer: 7); await tester.pump(); expect(find.text('Dragging'), findsOneWidget); expect(find.text('Target'), findsOneWidget); expect(find.text('Rejected'), findsNothing); final Offset secondLocation = tester.getCenter(find.text('Target')); await gesture.moveTo(secondLocation); await tester.pump(); expect(find.text('Dragging'), findsOneWidget); expect(find.text('Target'), findsNothing); expect(find.text('Rejected'), findsOneWidget); await gesture.up(); await tester.pump(); expect(find.text('Dragging'), findsNothing); expect(find.text('Target'), findsOneWidget); expect(find.text('Rejected'), findsNothing); expect(numberOfTimesOnDraggableCanceledCalled, 1); // Drag and drop the Draggable onto the Target a second time. final TestGesture secondGesture = await tester.startGesture(firstLocation, pointer: 7); await tester.pump(); expect(find.text('Dragging'), findsOneWidget); expect(find.text('Target'), findsOneWidget); expect(find.text('Rejected'), findsNothing); await secondGesture.moveTo(secondLocation); await tester.pump(); expect(find.text('Dragging'), findsOneWidget); expect(find.text('Target'), findsNothing); expect(find.text('Rejected'), findsOneWidget); await secondGesture.up(); await tester.pump(); expect(numberOfTimesOnDraggableCanceledCalled, 2); expect(find.text('Dragging'), findsNothing); expect(find.text('Target'), findsOneWidget); expect(find.text('Rejected'), findsNothing); }); testWidgets('Drag and drop - onDragCompleted not called if dropped on non-accepting target', (WidgetTester tester) async { final List<int> accepted = <int>[]; final List<DragTargetDetails<int>> acceptedDetails = <DragTargetDetails<int>>[]; bool onDragCompletedCalled = false; await tester.pumpWidget(MaterialApp( home: Column( children: <Widget>[ Draggable<int>( data: 1, child: const Text('Source'), feedback: const Text('Dragging'), onDragCompleted: () { onDragCompletedCalled = true; }, ), DragTarget<int>( builder: (BuildContext context, List<int?> data, List<dynamic> rejects) { return const SizedBox( height: 100.0, child: Text('Target'), ); }, onWillAccept: (int? data) => false, onAccept: accepted.add, onAcceptWithDetails: acceptedDetails.add, ), ], ), )); expect(accepted, isEmpty); expect(acceptedDetails, isEmpty); expect(find.text('Source'), findsOneWidget); expect(find.text('Dragging'), findsNothing); expect(find.text('Target'), findsOneWidget); expect(onDragCompletedCalled, isFalse); final Offset firstLocation = tester.getTopLeft(find.text('Source')); final TestGesture gesture = await tester.startGesture(firstLocation, pointer: 7); await tester.pump(); expect(accepted, isEmpty); expect(acceptedDetails, isEmpty); expect(find.text('Source'), findsOneWidget); expect(find.text('Dragging'), findsOneWidget); expect(find.text('Target'), findsOneWidget); expect(onDragCompletedCalled, isFalse); final Offset secondLocation = tester.getCenter(find.text('Target')); await gesture.moveTo(secondLocation); await tester.pump(); expect(accepted, isEmpty); expect(acceptedDetails, isEmpty); expect(find.text('Source'), findsOneWidget); expect(find.text('Dragging'), findsOneWidget); expect(find.text('Target'), findsOneWidget); expect(onDragCompletedCalled, isFalse); await gesture.up(); await tester.pump(); expect(accepted, isEmpty); expect(acceptedDetails, isEmpty); expect(find.text('Source'), findsOneWidget); expect(find.text('Dragging'), findsNothing); expect(find.text('Target'), findsOneWidget); expect(onDragCompletedCalled, isFalse); }); testWidgets('Drag and drop - onDragEnd called if dropped on accepting target', (WidgetTester tester) async { final List<int> accepted = <int>[]; final List<DragTargetDetails<int>> acceptedDetails = <DragTargetDetails<int>>[]; bool onDragEndCalled = false; late DraggableDetails onDragEndDraggableDetails; await tester.pumpWidget(MaterialApp( home: Column( children: <Widget>[ Draggable<int>( data: 1, child: const Text('Source'), feedback: const Text('Dragging'), onDragEnd: (DraggableDetails details) { onDragEndCalled = true; onDragEndDraggableDetails = details; }, ), DragTarget<int>( builder: (BuildContext context, List<int?> data, List<dynamic> rejects) { return const SizedBox(height: 100.0, child: Text('Target')); }, onAccept: accepted.add, onAcceptWithDetails: acceptedDetails.add, ), ], ), )); expect(accepted, isEmpty); expect(acceptedDetails, isEmpty); expect(find.text('Source'), findsOneWidget); expect(find.text('Dragging'), findsNothing); expect(find.text('Target'), findsOneWidget); expect(onDragEndCalled, isFalse); final Offset firstLocation = tester.getCenter(find.text('Source')); final TestGesture gesture = await tester.startGesture(firstLocation, pointer: 7); await tester.pump(); expect(accepted, isEmpty); expect(acceptedDetails, isEmpty); expect(find.text('Source'), findsOneWidget); expect(find.text('Dragging'), findsOneWidget); expect(find.text('Target'), findsOneWidget); expect(onDragEndCalled, isFalse); final Offset secondLocation = tester.getCenter(find.text('Target')); await gesture.moveTo(secondLocation); await tester.pump(); expect(accepted, isEmpty); expect(acceptedDetails, isEmpty); expect(find.text('Source'), findsOneWidget); expect(find.text('Dragging'), findsOneWidget); expect(find.text('Target'), findsOneWidget); expect(onDragEndCalled, isFalse); await gesture.up(); await tester.pump(); final Offset droppedLocation = tester.getTopLeft(find.text('Target')); final Offset expectedDropOffset = Offset(droppedLocation.dx, secondLocation.dy - firstLocation.dy); expect(accepted, equals(<int>[1])); expect(acceptedDetails, hasLength(1)); expect(acceptedDetails.first.offset, const Offset(256.0, 74.0)); expect(find.text('Source'), findsOneWidget); expect(find.text('Dragging'), findsNothing); expect(find.text('Target'), findsOneWidget); expect(onDragEndCalled, isTrue); expect(onDragEndDraggableDetails, isNotNull); expect(onDragEndDraggableDetails.wasAccepted, isTrue); expect(onDragEndDraggableDetails.velocity, equals(Velocity.zero)); expect(onDragEndDraggableDetails.offset, equals(expectedDropOffset)); }); testWidgets('DragTarget does not call onDragEnd when remove from the tree', (WidgetTester tester) async { final List<String> events = <String>[]; Offset firstLocation, secondLocation; int timesOnDragEndCalled = 0; await tester.pumpWidget(MaterialApp( home: Column( children: <Widget>[ Draggable<int>( data: 1, child: const Text('Source'), feedback: const Text('Dragging'), onDragEnd: (DraggableDetails details) { timesOnDragEndCalled++; }, ), DragTarget<int>( builder: (BuildContext context, List<int?> data, List<dynamic> rejects) { return const Text('Target'); }, onAccept: (int? data) { events.add('drop'); }, onAcceptWithDetails: (DragTargetDetails<int> _) { events.add('details'); }, ), ], ), )); expect(events, isEmpty); expect(find.text('Source'), findsOneWidget); expect(find.text('Target'), findsOneWidget); expect(events, isEmpty); await tester.tap(find.text('Source')); expect(events, isEmpty); firstLocation = tester.getCenter(find.text('Source')); final TestGesture gesture = await tester.startGesture(firstLocation, pointer: 7); await tester.pump(); await tester.pump(const Duration(seconds: 20)); secondLocation = tester.getCenter(find.text('Target')); await gesture.moveTo(secondLocation); await tester.pump(); await tester.pumpWidget(MaterialApp( home: Column( children: const <Widget>[ Draggable<int>( data: 1, child: Text('Source'), feedback: Text('Dragging'), ), ], ), )); expect(events, isEmpty); expect(timesOnDragEndCalled, equals(1)); await gesture.up(); await tester.pump(); }); testWidgets('Drag and drop - onDragCompleted called if dropped on accepting target', (WidgetTester tester) async { final List<int> accepted = <int>[]; final List<DragTargetDetails<int>> acceptedDetails = <DragTargetDetails<int>>[]; bool onDragCompletedCalled = false; await tester.pumpWidget(MaterialApp( home: Column( children: <Widget>[ Draggable<int>( data: 1, child: const Text('Source'), feedback: const Text('Dragging'), onDragCompleted: () { onDragCompletedCalled = true; }, ), DragTarget<int>( builder: (BuildContext context, List<int?> data, List<dynamic> rejects) { return const SizedBox(height: 100.0, child: Text('Target')); }, onAccept: accepted.add, onAcceptWithDetails: acceptedDetails.add, ), ], ), )); expect(accepted, isEmpty); expect(acceptedDetails, isEmpty); expect(find.text('Source'), findsOneWidget); expect(find.text('Dragging'), findsNothing); expect(find.text('Target'), findsOneWidget); expect(onDragCompletedCalled, isFalse); final Offset firstLocation = tester.getCenter(find.text('Source')); final TestGesture gesture = await tester.startGesture(firstLocation, pointer: 7); await tester.pump(); expect(accepted, isEmpty); expect(acceptedDetails, isEmpty); expect(find.text('Source'), findsOneWidget); expect(find.text('Dragging'), findsOneWidget); expect(find.text('Target'), findsOneWidget); expect(onDragCompletedCalled, isFalse); final Offset secondLocation = tester.getCenter(find.text('Target')); await gesture.moveTo(secondLocation); await tester.pump(); expect(accepted, isEmpty); expect(acceptedDetails, isEmpty); expect(find.text('Source'), findsOneWidget); expect(find.text('Dragging'), findsOneWidget); expect(find.text('Target'), findsOneWidget); expect(onDragCompletedCalled, isFalse); await gesture.up(); await tester.pump(); expect(accepted, equals(<int>[1])); expect(acceptedDetails, hasLength(1)); expect(acceptedDetails.first.offset, const Offset(256.0, 74.0)); expect(find.text('Source'), findsOneWidget); expect(find.text('Dragging'), findsNothing); expect(find.text('Target'), findsOneWidget); expect(onDragCompletedCalled, isTrue); }); testWidgets('Drag and drop - allow pass thru of unaccepted data test', (WidgetTester tester) async { final List<int> acceptedInts = <int>[]; final List<DragTargetDetails<int>> acceptedIntsDetails = <DragTargetDetails<int>>[]; final List<double> acceptedDoubles = <double>[]; final List<DragTargetDetails<double>> acceptedDoublesDetails = <DragTargetDetails<double>>[]; await tester.pumpWidget(MaterialApp( home: Column( children: <Widget>[ const Draggable<int>( data: 1, child: Text('IntSource'), feedback: Text('IntDragging'), ), const Draggable<double>( data: 1.0, child: Text('DoubleSource'), feedback: Text('DoubleDragging'), ), Stack( children: <Widget>[ DragTarget<int>( builder: (BuildContext context, List<int?> data, List<dynamic> rejects) { return const IgnorePointer( child: SizedBox( height: 100.0, child: Text('Target1'), ), ); }, onAccept: acceptedInts.add, onAcceptWithDetails: acceptedIntsDetails.add, ), DragTarget<double>( builder: (BuildContext context, List<double?> data, List<dynamic> rejects) { return const IgnorePointer( child: SizedBox( height: 100.0, child: Text('Target2'), ), ); }, onAccept: acceptedDoubles.add, onAcceptWithDetails: acceptedDoublesDetails.add, ), ], ), ], ), )); expect(acceptedInts, isEmpty); expect(acceptedIntsDetails, isEmpty); expect(acceptedDoubles, isEmpty); expect(acceptedDoublesDetails, isEmpty); expect(find.text('IntSource'), findsOneWidget); expect(find.text('IntDragging'), findsNothing); expect(find.text('DoubleSource'), findsOneWidget); expect(find.text('DoubleDragging'), findsNothing); expect(find.text('Target1'), findsOneWidget); expect(find.text('Target2'), findsOneWidget); final Offset intLocation = tester.getCenter(find.text('IntSource')); final Offset doubleLocation = tester.getCenter(find.text('DoubleSource')); final Offset targetLocation = tester.getCenter(find.text('Target1')); // Drag the double draggable. final TestGesture doubleGesture = await tester.startGesture(doubleLocation, pointer: 7); await tester.pump(); expect(acceptedInts, isEmpty); expect(acceptedIntsDetails, isEmpty); expect(acceptedDoubles, isEmpty); expect(acceptedDoublesDetails, isEmpty); expect(find.text('IntDragging'), findsNothing); expect(find.text('DoubleDragging'), findsOneWidget); await doubleGesture.moveTo(targetLocation); await tester.pump(); expect(acceptedInts, isEmpty); expect(acceptedIntsDetails, isEmpty); expect(acceptedDoubles, isEmpty); expect(acceptedDoublesDetails, isEmpty); expect(find.text('IntDragging'), findsNothing); expect(find.text('DoubleDragging'), findsOneWidget); await doubleGesture.up(); await tester.pump(); expect(acceptedInts, isEmpty); expect(acceptedIntsDetails, isEmpty); expect(acceptedDoubles, equals(<double>[1.0])); expect(acceptedDoublesDetails, hasLength(1)); expect(acceptedDoublesDetails.first.offset, const Offset(112.0, 122.0)); expect(find.text('IntDragging'), findsNothing); expect(find.text('DoubleDragging'), findsNothing); acceptedDoubles.clear(); acceptedDoublesDetails.clear(); // Drag the int draggable. final TestGesture intGesture = await tester.startGesture(intLocation, pointer: 7); await tester.pump(); expect(acceptedInts, isEmpty); expect(acceptedIntsDetails, isEmpty); expect(acceptedDoubles, isEmpty); expect(acceptedDoublesDetails, isEmpty); expect(find.text('IntDragging'), findsOneWidget); expect(find.text('DoubleDragging'), findsNothing); await intGesture.moveTo(targetLocation); await tester.pump(); expect(acceptedInts, isEmpty); expect(acceptedIntsDetails, isEmpty); expect(acceptedDoubles, isEmpty); expect(acceptedDoublesDetails, isEmpty); expect(find.text('IntDragging'), findsOneWidget); expect(find.text('DoubleDragging'), findsNothing); await intGesture.up(); await tester.pump(); expect(acceptedInts, equals(<int>[1])); expect(acceptedIntsDetails, hasLength(1)); expect(acceptedIntsDetails.first.offset, const Offset(184.0, 122.0)); expect(acceptedDoubles, isEmpty); expect(acceptedDoublesDetails, isEmpty); expect(find.text('IntDragging'), findsNothing); expect(find.text('DoubleDragging'), findsNothing); }); testWidgets('Drag and drop - allow pass thru of unaccepted data twice test', (WidgetTester tester) async { final List<DragTargetData> acceptedDragTargetDatas = <DragTargetData>[]; final List<DragTargetDetails<DragTargetData>> acceptedDragTargetDataDetails = <DragTargetDetails<DragTargetData>>[]; final List<ExtendedDragTargetData> acceptedExtendedDragTargetDatas = <ExtendedDragTargetData>[]; final List<DragTargetDetails<ExtendedDragTargetData>> acceptedExtendedDragTargetDataDetails = <DragTargetDetails<ExtendedDragTargetData>>[]; final DragTargetData dragTargetData = DragTargetData(); await tester.pumpWidget(MaterialApp( home: Column( children: <Widget>[ Draggable<DragTargetData>( data: dragTargetData, child: const Text('Source'), feedback: const Text('Dragging'), ), Stack( children: <Widget>[ DragTarget<DragTargetData>( builder: (BuildContext context, List<DragTargetData?> data, List<dynamic> rejects) { return const IgnorePointer( child: SizedBox( height: 100.0, child: Text('Target1'), ), ); }, onAccept: acceptedDragTargetDatas.add, onAcceptWithDetails: acceptedDragTargetDataDetails.add, ), DragTarget<ExtendedDragTargetData>( builder: (BuildContext context, List<ExtendedDragTargetData?> data, List<dynamic> rejects) { return const IgnorePointer( child: SizedBox( height: 100.0, child: Text('Target2'), ), ); }, onAccept: acceptedExtendedDragTargetDatas.add, onAcceptWithDetails: acceptedExtendedDragTargetDataDetails.add, ), ], ), ], ), )); final Offset dragTargetLocation = tester.getCenter(find.text('Source')); final Offset targetLocation = tester.getCenter(find.text('Target1')); for (int i = 0; i < 2; i += 1) { final TestGesture gesture = await tester.startGesture(dragTargetLocation); await tester.pump(); await gesture.moveTo(targetLocation); await tester.pump(); await gesture.up(); await tester.pump(); expect(acceptedDragTargetDatas, equals(<DragTargetData>[dragTargetData])); expect(acceptedDragTargetDataDetails, hasLength(1)); expect(acceptedDragTargetDataDetails.first.offset, const Offset(256.0, 74.0)); expect(acceptedExtendedDragTargetDatas, isEmpty); expect(acceptedExtendedDragTargetDataDetails, isEmpty); acceptedDragTargetDatas.clear(); acceptedDragTargetDataDetails.clear(); await tester.pump(); } }); testWidgets('Drag and drop - maxSimultaneousDrags', (WidgetTester tester) async { final List<int> accepted = <int>[]; final List<DragTargetDetails<int>> acceptedDetails = <DragTargetDetails<int>>[]; Widget build(int maxSimultaneousDrags) { return MaterialApp( home: Column( children: <Widget>[ Draggable<int>( data: 1, maxSimultaneousDrags: maxSimultaneousDrags, child: const Text('Source'), feedback: const Text('Dragging'), ), DragTarget<int>( builder: (BuildContext context, List<int?> data, List<dynamic> rejects) { return const SizedBox(height: 100.0, child: Text('Target')); }, onAccept: accepted.add, onAcceptWithDetails: acceptedDetails.add, ), ], ), ); } await tester.pumpWidget(build(0)); final Offset firstLocation = tester.getCenter(find.text('Source')); final Offset secondLocation = tester.getCenter(find.text('Target')); expect(accepted, isEmpty); expect(acceptedDetails, isEmpty); expect(find.text('Source'), findsOneWidget); expect(find.text('Dragging'), findsNothing); expect(find.text('Target'), findsOneWidget); final TestGesture gesture = await tester.startGesture(firstLocation, pointer: 7); await tester.pump(); expect(accepted, isEmpty); expect(acceptedDetails, isEmpty); expect(find.text('Source'), findsOneWidget); expect(find.text('Dragging'), findsNothing); expect(find.text('Target'), findsOneWidget); await gesture.up(); await tester.pumpWidget(build(2)); expect(accepted, isEmpty); expect(acceptedDetails, isEmpty); expect(find.text('Source'), findsOneWidget); expect(find.text('Dragging'), findsNothing); expect(find.text('Target'), findsOneWidget); final TestGesture gesture1 = await tester.startGesture(firstLocation, pointer: 8); await tester.pump(); expect(accepted, isEmpty); expect(acceptedDetails, isEmpty); expect(find.text('Source'), findsOneWidget); expect(find.text('Dragging'), findsOneWidget); expect(find.text('Target'), findsOneWidget); final TestGesture gesture2 = await tester.startGesture(firstLocation, pointer: 9); await tester.pump(); expect(accepted, isEmpty); expect(acceptedDetails, isEmpty); expect(find.text('Source'), findsOneWidget); expect(find.text('Dragging'), findsNWidgets(2)); expect(find.text('Target'), findsOneWidget); final TestGesture gesture3 = await tester.startGesture(firstLocation, pointer: 10); await tester.pump(); expect(accepted, isEmpty); expect(acceptedDetails, isEmpty); expect(find.text('Source'), findsOneWidget); expect(find.text('Dragging'), findsNWidgets(2)); expect(find.text('Target'), findsOneWidget); await gesture1.moveTo(secondLocation); await gesture2.moveTo(secondLocation); await gesture3.moveTo(secondLocation); await tester.pump(); expect(accepted, isEmpty); expect(acceptedDetails, isEmpty); expect(find.text('Source'), findsOneWidget); expect(find.text('Dragging'), findsNWidgets(2)); expect(find.text('Target'), findsOneWidget); await gesture1.up(); await tester.pump(); expect(accepted, equals(<int>[1])); expect(acceptedDetails, hasLength(1)); expect(acceptedDetails.first.offset, const Offset(256.0, 74.0)); expect(find.text('Source'), findsOneWidget); expect(find.text('Dragging'), findsOneWidget); expect(find.text('Target'), findsOneWidget); await gesture2.up(); await tester.pump(); expect(accepted, equals(<int>[1, 1])); expect(acceptedDetails, hasLength(2)); expect(acceptedDetails[0].offset, const Offset(256.0, 74.0)); expect(acceptedDetails[1].offset, const Offset(256.0, 74.0)); expect(find.text('Source'), findsOneWidget); expect(find.text('Dragging'), findsNothing); expect(find.text('Target'), findsOneWidget); await gesture3.up(); await tester.pump(); expect(accepted, equals(<int>[1, 1])); expect(acceptedDetails, hasLength(2)); expect(acceptedDetails[0].offset, const Offset(256.0, 74.0)); expect(acceptedDetails[1].offset, const Offset(256.0, 74.0)); expect(find.text('Source'), findsOneWidget); expect(find.text('Dragging'), findsNothing); expect(find.text('Target'), findsOneWidget); }); testWidgets('Draggable disposes recognizer', (WidgetTester tester) async { bool didTap = false; await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Overlay( initialEntries: <OverlayEntry>[ OverlayEntry( builder: (BuildContext context) => GestureDetector( onTap: () { didTap = true; }, child: Draggable<Object>( child: Container( color: const Color(0xFFFFFF00), ), feedback: Container( width: 100.0, height: 100.0, color: const Color(0xFFFF0000), ), ), ), ), ], ), ), ); await tester.startGesture(const Offset(10.0, 10.0)); expect(didTap, isFalse); // This tears down the draggable without terminating the gesture sequence, // which used to trigger asserts in the multi-drag gesture recognizer. await tester.pumpWidget(Container(key: UniqueKey())); expect(didTap, isFalse); }); // Regression test for https://github.com/flutter/flutter/issues/6128. testWidgets('Draggable plays nice with onTap', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Overlay( initialEntries: <OverlayEntry>[ OverlayEntry( builder: (BuildContext context) => GestureDetector( onTap: () { /* registers a tap recognizer */ }, child: Draggable<Object>( child: Container( color: const Color(0xFFFFFF00), ), feedback: Container( width: 100.0, height: 100.0, color: const Color(0xFFFF0000), ), ), ), ), ], ), ), ); final TestGesture firstGesture = await tester.startGesture(const Offset(10.0, 10.0), pointer: 24); final TestGesture secondGesture = await tester.startGesture(const Offset(10.0, 20.0), pointer: 25); await firstGesture.moveBy(const Offset(100.0, 0.0)); await secondGesture.up(); }); testWidgets('DragTarget does not set state when remove from the tree', (WidgetTester tester) async { final List<String> events = <String>[]; Offset firstLocation, secondLocation; await tester.pumpWidget(MaterialApp( home: Column( children: <Widget>[ const Draggable<int>( data: 1, child: Text('Source'), feedback: Text('Dragging'), ), DragTarget<int>( builder: (BuildContext context, List<int?> data, List<dynamic> rejects) { return const Text('Target'); }, onAccept: (int? data) { events.add('drop'); }, onAcceptWithDetails: (DragTargetDetails<int> _) { events.add('details'); }, ), ], ), )); expect(events, isEmpty); expect(find.text('Source'), findsOneWidget); expect(find.text('Target'), findsOneWidget); expect(events, isEmpty); await tester.tap(find.text('Source')); expect(events, isEmpty); firstLocation = tester.getCenter(find.text('Source')); final TestGesture gesture = await tester.startGesture(firstLocation, pointer: 7); await tester.pump(); await tester.pump(const Duration(seconds: 20)); secondLocation = tester.getCenter(find.text('Target')); await gesture.moveTo(secondLocation); await tester.pump(); await tester.pumpWidget(MaterialApp( home: Column( children: const <Widget>[ Draggable<int>( data: 1, child: Text('Source'), feedback: Text('Dragging'), ), ], ), )); expect(events, isEmpty); await gesture.up(); await tester.pump(); }); testWidgets('Drag and drop - remove draggable', (WidgetTester tester) async { final List<int> accepted = <int>[]; final List<DragTargetDetails<int>> acceptedDetails = <DragTargetDetails<int>>[]; await tester.pumpWidget(MaterialApp( home: Column( children: <Widget>[ const Draggable<int>( data: 1, child: Text('Source'), feedback: Text('Dragging'), ), DragTarget<int>( builder: (BuildContext context, List<int?> data, List<dynamic> rejects) { return const SizedBox(height: 100.0, child: Text('Target')); }, onAccept: accepted.add, onAcceptWithDetails: acceptedDetails.add, ), ], ), )); expect(accepted, isEmpty); expect(acceptedDetails, isEmpty); expect(find.text('Source'), findsOneWidget); expect(find.text('Dragging'), findsNothing); expect(find.text('Target'), findsOneWidget); final Offset firstLocation = tester.getCenter(find.text('Source')); final TestGesture gesture = await tester.startGesture(firstLocation, pointer: 7); await tester.pump(); expect(accepted, isEmpty); expect(acceptedDetails, isEmpty); expect(find.text('Source'), findsOneWidget); expect(find.text('Dragging'), findsOneWidget); expect(find.text('Target'), findsOneWidget); await tester.pumpWidget(MaterialApp( home: Column( children: <Widget>[ DragTarget<int>( builder: (BuildContext context, List<int?> data, List<dynamic> rejects) { return const SizedBox(height: 100.0, child: Text('Target')); }, onAccept: accepted.add, onAcceptWithDetails: acceptedDetails.add, ), ], ), )); expect(accepted, isEmpty); expect(acceptedDetails, isEmpty); expect(find.text('Source'), findsNothing); expect(find.text('Dragging'), findsOneWidget); expect(find.text('Target'), findsOneWidget); final Offset secondLocation = tester.getCenter(find.text('Target')); await gesture.moveTo(secondLocation); await tester.pump(); expect(accepted, isEmpty); expect(acceptedDetails, isEmpty); expect(find.text('Source'), findsNothing); expect(find.text('Dragging'), findsOneWidget); expect(find.text('Target'), findsOneWidget); await gesture.up(); await tester.pump(); expect(accepted, equals(<int>[1])); expect(acceptedDetails, hasLength(1)); expect(acceptedDetails.first.offset, const Offset(256.0, 26.0)); expect(find.text('Source'), findsNothing); expect(find.text('Dragging'), findsNothing); expect(find.text('Target'), findsOneWidget); }); testWidgets('Tap above long-press draggable works', (WidgetTester tester) async { final List<String> events = <String>[]; await tester.pumpWidget(MaterialApp( home: Material( child: Center( child: GestureDetector( onTap: () { events.add('tap'); }, child: const LongPressDraggable<int>( feedback: Text('Feedback'), child: Text('X'), ), ), ), ), )); expect(events, isEmpty); await tester.tap(find.text('X')); expect(events, equals(<String>['tap'])); }); testWidgets('long-press draggable calls onDragEnd called if dropped on accepting target', (WidgetTester tester) async { final List<int> accepted = <int>[]; final List<DragTargetDetails<int>> acceptedDetails = <DragTargetDetails<int>>[]; bool onDragEndCalled = false; late DraggableDetails onDragEndDraggableDetails; await tester.pumpWidget(MaterialApp( home: Column( children: <Widget>[ LongPressDraggable<int>( data: 1, child: const Text('Source'), feedback: const Text('Dragging'), onDragEnd: (DraggableDetails details) { onDragEndCalled = true; onDragEndDraggableDetails = details; }, ), DragTarget<int>( builder: (BuildContext context, List<int?> data, List<dynamic> rejects) { return const SizedBox(height: 100.0, child: Text('Target')); }, onAccept: accepted.add, onAcceptWithDetails: acceptedDetails.add, ), ], ), )); expect(accepted, isEmpty); expect(acceptedDetails, isEmpty); expect(find.text('Source'), findsOneWidget); expect(find.text('Dragging'), findsNothing); expect(find.text('Target'), findsOneWidget); expect(onDragEndCalled, isFalse); final Offset firstLocation = tester.getCenter(find.text('Source')); final TestGesture gesture = await tester.startGesture(firstLocation, pointer: 7); await tester.pump(); expect(accepted, isEmpty); expect(acceptedDetails, isEmpty); expect(find.text('Source'), findsOneWidget); expect(find.text('Dragging'), findsNothing); expect(find.text('Target'), findsOneWidget); expect(onDragEndCalled, isFalse); await tester.pump(kLongPressTimeout); expect(accepted, isEmpty); expect(acceptedDetails, isEmpty); expect(find.text('Source'), findsOneWidget); expect(find.text('Dragging'), findsOneWidget); expect(find.text('Target'), findsOneWidget); expect(onDragEndCalled, isFalse); final Offset secondLocation = tester.getCenter(find.text('Target')); await gesture.moveTo(secondLocation); await tester.pump(); expect(accepted, isEmpty); expect(acceptedDetails, isEmpty); expect(find.text('Source'), findsOneWidget); expect(find.text('Dragging'), findsOneWidget); expect(find.text('Target'), findsOneWidget); expect(onDragEndCalled, isFalse); await gesture.up(); await tester.pump(); final Offset droppedLocation = tester.getTopLeft(find.text('Target')); final Offset expectedDropOffset = Offset(droppedLocation.dx, secondLocation.dy - firstLocation.dy); expect(accepted, equals(<int>[1])); expect(acceptedDetails, hasLength(1)); expect(acceptedDetails.first.offset, expectedDropOffset); expect(find.text('Source'), findsOneWidget); expect(find.text('Dragging'), findsNothing); expect(find.text('Target'), findsOneWidget); expect(onDragEndCalled, isTrue); expect(onDragEndDraggableDetails, isNotNull); expect(onDragEndDraggableDetails.wasAccepted, isTrue); expect(onDragEndDraggableDetails.velocity, equals(Velocity.zero)); expect(onDragEndDraggableDetails.offset, equals(expectedDropOffset)); }); testWidgets('long-press draggable calls onDragCompleted called if dropped on accepting target', (WidgetTester tester) async { final List<int> accepted = <int>[]; final List<DragTargetDetails<int>> acceptedDetails = <DragTargetDetails<int>>[]; bool onDragCompletedCalled = false; await tester.pumpWidget(MaterialApp( home: Column( children: <Widget>[ LongPressDraggable<int>( data: 1, child: const Text('Source'), feedback: const Text('Dragging'), onDragCompleted: () { onDragCompletedCalled = true; }, ), DragTarget<int>( builder: (BuildContext context, List<int?> data, List<dynamic> rejects) { return const SizedBox(height: 100.0, child: Text('Target')); }, onAccept: accepted.add, onAcceptWithDetails: acceptedDetails.add, ), ], ), )); expect(accepted, isEmpty); expect(acceptedDetails, isEmpty); expect(find.text('Source'), findsOneWidget); expect(find.text('Dragging'), findsNothing); expect(find.text('Target'), findsOneWidget); expect(onDragCompletedCalled, isFalse); final Offset firstLocation = tester.getCenter(find.text('Source')); final TestGesture gesture = await tester.startGesture(firstLocation, pointer: 7); await tester.pump(); expect(accepted, isEmpty); expect(acceptedDetails, isEmpty); expect(find.text('Source'), findsOneWidget); expect(find.text('Dragging'), findsNothing); expect(find.text('Target'), findsOneWidget); expect(onDragCompletedCalled, isFalse); await tester.pump(kLongPressTimeout); expect(accepted, isEmpty); expect(acceptedDetails, isEmpty); expect(find.text('Source'), findsOneWidget); expect(find.text('Dragging'), findsOneWidget); expect(find.text('Target'), findsOneWidget); expect(onDragCompletedCalled, isFalse); final Offset secondLocation = tester.getCenter(find.text('Target')); await gesture.moveTo(secondLocation); await tester.pump(); expect(accepted, isEmpty); expect(acceptedDetails, isEmpty); expect(find.text('Source'), findsOneWidget); expect(find.text('Dragging'), findsOneWidget); expect(find.text('Target'), findsOneWidget); expect(onDragCompletedCalled, isFalse); await gesture.up(); await tester.pump(); expect(accepted, equals(<int>[1])); expect(acceptedDetails.first.offset, const Offset(256.0, 74.0)); expect(find.text('Source'), findsOneWidget); expect(find.text('Dragging'), findsNothing); expect(find.text('Target'), findsOneWidget); expect(onDragCompletedCalled, isTrue); }); testWidgets('long-press draggable calls onDragStartedCalled after long press', (WidgetTester tester) async { bool onDragStartedCalled = false; await tester.pumpWidget(MaterialApp( home: LongPressDraggable<int>( data: 1, child: const Text('Source'), feedback: const Text('Dragging'), onDragStarted: () { onDragStartedCalled = true; }, ), )); expect(find.text('Source'), findsOneWidget); expect(find.text('Dragging'), findsNothing); expect(onDragStartedCalled, isFalse); final Offset firstLocation = tester.getCenter(find.text('Source')); await tester.startGesture(firstLocation, pointer: 7); await tester.pump(); expect(find.text('Source'), findsOneWidget); expect(find.text('Dragging'), findsNothing); expect(onDragStartedCalled, isFalse); await tester.pump(kLongPressTimeout); expect(find.text('Source'), findsOneWidget); expect(find.text('Dragging'), findsOneWidget); expect(onDragStartedCalled, isTrue); }); testWidgets('Custom long press delay for LongPressDraggable', (WidgetTester tester) async { bool onDragStartedCalled = false; await tester.pumpWidget(MaterialApp( home: LongPressDraggable<int>( data: 1, delay: const Duration(seconds: 2), child: const Text('Source'), feedback: const Text('Dragging'), onDragStarted: () { onDragStartedCalled = true; }, ), )); expect(find.text('Source'), findsOneWidget); expect(find.text('Dragging'), findsNothing); expect(onDragStartedCalled, isFalse); final Offset firstLocation = tester.getCenter(find.text('Source')); await tester.startGesture(firstLocation, pointer: 7); await tester.pump(); expect(find.text('Source'), findsOneWidget); expect(find.text('Dragging'), findsNothing); expect(onDragStartedCalled, isFalse); // Halfway into the long press duration. await tester.pump(const Duration(seconds: 1)); expect(find.text('Source'), findsOneWidget); expect(find.text('Dragging'), findsNothing); expect(onDragStartedCalled, isFalse); // Long press draggable should be showing. await tester.pump(const Duration(seconds: 1)); expect(find.text('Source'), findsOneWidget); expect(find.text('Dragging'), findsOneWidget); expect(onDragStartedCalled, isTrue); }); testWidgets('Default long press delay for LongPressDraggable', (WidgetTester tester) async { bool onDragStartedCalled = false; await tester.pumpWidget(MaterialApp( home: LongPressDraggable<int>( data: 1, child: const Text('Source'), feedback: const Text('Dragging'), onDragStarted: () { onDragStartedCalled = true; }, ), )); expect(find.text('Source'), findsOneWidget); expect(find.text('Dragging'), findsNothing); expect(onDragStartedCalled, isFalse); final Offset firstLocation = tester.getCenter(find.text('Source')); await tester.startGesture(firstLocation, pointer: 7); await tester.pump(); expect(find.text('Source'), findsOneWidget); expect(find.text('Dragging'), findsNothing); expect(onDragStartedCalled, isFalse); // Halfway into the long press duration. await tester.pump(const Duration(milliseconds: 250)); expect(find.text('Source'), findsOneWidget); expect(find.text('Dragging'), findsNothing); expect(onDragStartedCalled, isFalse); // Long press draggable should be showing. await tester.pump(const Duration(milliseconds: 250)); expect(find.text('Source'), findsOneWidget); expect(find.text('Dragging'), findsOneWidget); expect(onDragStartedCalled, isTrue); }); testWidgets('long-press draggable calls Haptic Feedback onStart', (WidgetTester tester) async { await _testLongPressDraggableHapticFeedback(tester: tester, hapticFeedbackOnStart: true, expectedHapticFeedbackCount: 1); }); testWidgets('long-press draggable can disable Haptic Feedback', (WidgetTester tester) async { await _testLongPressDraggableHapticFeedback(tester: tester, hapticFeedbackOnStart: false, expectedHapticFeedbackCount: 0); }); testWidgets('Drag feedback with child anchor positions correctly', (WidgetTester tester) async { await _testChildAnchorFeedbackPosition(tester: tester); }); testWidgets('Drag feedback with child anchor within a non-global Overlay positions correctly', (WidgetTester tester) async { await _testChildAnchorFeedbackPosition(tester: tester, left: 100.0, top: 100.0); }); testWidgets('Drag feedback is put on root overlay with [rootOverlay] flag', (WidgetTester tester) async { final GlobalKey<NavigatorState> rootNavigatorKey = GlobalKey<NavigatorState>(); final GlobalKey<NavigatorState> childNavigatorKey = GlobalKey<NavigatorState>(); // Create a [MaterialApp], with a nested [Navigator], which has the // [Draggable]. await tester.pumpWidget(MaterialApp( navigatorKey: rootNavigatorKey, home: Column( children: <Widget>[ SizedBox( height: 200.0, child: Navigator( key: childNavigatorKey, onGenerateRoute: (RouteSettings settings) { if (settings.name == '/') { return MaterialPageRoute<void>( settings: settings, builder: (BuildContext context) => const Draggable<int>( data: 1, child: Text('Source'), feedback: Text('Dragging'), rootOverlay: true, ), ); } throw UnsupportedError('Unsupported route: $settings'); }, ), ), DragTarget<int>( builder: (BuildContext context, List<int?> data, List<dynamic> rejects) { return const SizedBox( height: 300.0, child: Center(child: Text('Target 1')), ); }, ), ], ), )); final Offset firstLocation = tester.getCenter(find.text('Source')); final TestGesture gesture = await tester.startGesture(firstLocation, pointer: 7); await tester.pump(); final Offset secondLocation = tester.getCenter(find.text('Target 1')); await gesture.moveTo(secondLocation); await tester.pump(); // Expect that the feedback widget is a descendant of the root overlay, // but not a descendant of the child overlay. expect( find.descendant( of: find.byType(Overlay).first, matching: find.text('Dragging'), ), findsOneWidget, ); expect( find.descendant( of: find.byType(Overlay).last, matching: find.text('Dragging'), ), findsNothing, ); }); // Regression test for https://github.com/flutter/flutter/issues/72483 testWidgets('Drag and drop - DragTarget<Object> can accept Draggable<int> data', (WidgetTester tester) async { final List<Object> accepted = <Object>[]; await tester.pumpWidget(MaterialApp( home: Column( children: <Widget>[ const Draggable<int>( data: 1, child: Text('Source'), feedback: Text('Dragging'), ), DragTarget<Object>( builder: (BuildContext context, List<Object?> data, List<dynamic> rejects) { return const SizedBox(height: 100.0, child: Text('Target')); }, onAccept: accepted.add, ), ], ), )); expect(accepted, isEmpty); final Offset firstLocation = tester.getCenter(find.text('Source')); final TestGesture gesture = await tester.startGesture(firstLocation, pointer: 7); await tester.pump(); final Offset secondLocation = tester.getCenter(find.text('Target')); await gesture.moveTo(secondLocation); await tester.pump(); await gesture.up(); await tester.pump(); expect(accepted, equals(<int>[1])); }); testWidgets('Drag and drop - DragTarget<int> can accept Draggable<Object> data when runtime type is int', (WidgetTester tester) async { final List<int> accepted = <int>[]; await tester.pumpWidget(MaterialApp( home: Column( children: <Widget>[ const Draggable<Object>( data: 1, child: Text('Source'), feedback: Text('Dragging'), ), DragTarget<int>( builder: (BuildContext context, List<int?> data, List<dynamic> rejects) { return const SizedBox(height: 100.0, child: Text('Target')); }, onAccept: accepted.add, ), ], ), )); expect(accepted, isEmpty); final Offset firstLocation = tester.getCenter(find.text('Source')); final TestGesture gesture = await tester.startGesture(firstLocation, pointer: 7); await tester.pump(); final Offset secondLocation = tester.getCenter(find.text('Target')); await gesture.moveTo(secondLocation); await tester.pump(); await gesture.up(); await tester.pump(); expect(accepted, equals(<int>[1])); }); testWidgets('Drag and drop - DragTarget<int> should not accept Draggable<Object> data when runtime type null', (WidgetTester tester) async { final List<int> accepted = <int>[]; bool isReceiveNullDataForCheck = false; await tester.pumpWidget(MaterialApp( home: Column( children: <Widget>[ const Draggable<Object>( child: Text('Source'), feedback: Text('Dragging'), ), DragTarget<int>( builder: (BuildContext context, List<int?> data, List<dynamic> rejects) { return const SizedBox(height: 100.0, child: Text('Target')); }, onAccept: accepted.add, onWillAccept: (int? data) { if (data == null) isReceiveNullDataForCheck = true; return data != null; }, ), ], ), )); expect(accepted, isEmpty); final Offset firstLocation = tester.getCenter(find.text('Source')); final TestGesture gesture = await tester.startGesture(firstLocation, pointer: 7); await tester.pump(); final Offset secondLocation = tester.getCenter(find.text('Target')); await gesture.moveTo(secondLocation); await tester.pump(); await gesture.up(); await tester.pump(); expect(accepted, isEmpty); expect(isReceiveNullDataForCheck, true); }); testWidgets('Drag and drop can contribute semantics', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget(MaterialApp( home: ListView( scrollDirection: Axis.horizontal, addSemanticIndexes: false, children: <Widget>[ DragTarget<int>( builder: (BuildContext context, List<int?> data, List<dynamic> rejects) { return const Text('Target'); }, ), Container(width: 400.0), const Draggable<int>( data: 1, child: Text('H'), feedback: Text('H'), childWhenDragging: SizedBox(), axis: Axis.horizontal, ignoringFeedbackSemantics: false, ), const Draggable<int>( data: 2, child: Text('V'), feedback: Text('V'), childWhenDragging: SizedBox(), axis: Axis.vertical, ignoringFeedbackSemantics: false, ), const Draggable<int>( data: 3, child: Text('N'), feedback: Text('N'), childWhenDragging: SizedBox(), ), ], ), )); expect(semantics, hasSemantics( TestSemantics.root( children: <TestSemantics>[ TestSemantics( id: 1, textDirection: TextDirection.ltr, children: <TestSemantics>[ TestSemantics( id: 2, children: <TestSemantics>[ TestSemantics( id: 3, flags: <SemanticsFlag>[SemanticsFlag.scopesRoute], children: <TestSemantics>[ TestSemantics( id: 4, children: <TestSemantics>[ TestSemantics( id: 9, flags: <SemanticsFlag>[SemanticsFlag.hasImplicitScrolling], actions: <SemanticsAction>[SemanticsAction.scrollLeft], children: <TestSemantics>[ TestSemantics( id: 5, tags: <SemanticsTag>[const SemanticsTag('RenderViewport.twoPane')], label: 'Target', textDirection: TextDirection.ltr, ), TestSemantics( id: 6, tags: <SemanticsTag>[const SemanticsTag('RenderViewport.twoPane')], label: 'H', textDirection: TextDirection.ltr, ), TestSemantics( id: 7, tags: <SemanticsTag>[const SemanticsTag('RenderViewport.twoPane')], label: 'V', textDirection: TextDirection.ltr, ), TestSemantics( id: 8, tags: <SemanticsTag>[const SemanticsTag('RenderViewport.twoPane')], label: 'N', textDirection: TextDirection.ltr, ), ], ), ], ), ], ), ], ), ], ), ], ), ignoreTransform: true, ignoreRect: true, )); final Offset firstLocation = tester.getTopLeft(find.text('N')); final Offset secondLocation = firstLocation + const Offset(300.0, 300.0); final TestGesture gesture = await tester.startGesture(firstLocation, pointer: 7); await tester.pump(); await gesture.moveTo(secondLocation); await tester.pump(); expect(semantics, hasSemantics( TestSemantics.root( children: <TestSemantics>[ TestSemantics( id: 1, textDirection: TextDirection.ltr, children: <TestSemantics>[ TestSemantics( id: 2, children: <TestSemantics>[ TestSemantics( id: 3, flags: <SemanticsFlag>[SemanticsFlag.scopesRoute], children: <TestSemantics>[ TestSemantics( id: 4, children: <TestSemantics>[ TestSemantics( id: 9, flags: <SemanticsFlag>[SemanticsFlag.hasImplicitScrolling], children: <TestSemantics>[ TestSemantics( id: 5, tags: <SemanticsTag>[const SemanticsTag('RenderViewport.twoPane')], label: 'Target', textDirection: TextDirection.ltr, ), TestSemantics( id: 6, tags: <SemanticsTag>[const SemanticsTag('RenderViewport.twoPane')], label: 'H', textDirection: TextDirection.ltr, ), TestSemantics( id: 7, tags: <SemanticsTag>[const SemanticsTag('RenderViewport.twoPane')], label: 'V', textDirection: TextDirection.ltr, ), /// N is moved offscreen. ], ), ], ), ], ), ], ), ], ), ], ), ignoreTransform: true, ignoreRect: true, )); semantics.dispose(); }); testWidgets('Drag and drop - when a dragAnchorStrategy is provided it gets called', (WidgetTester tester) async { bool dragAnchorStrategyCalled = false; await tester.pumpWidget(MaterialApp( home: Column( children: <Widget>[ Draggable<int>( child: const Text('Source'), feedback: const Text('Feedback'), dragAnchorStrategy: (Draggable<Object> widget, BuildContext context, Offset position) { dragAnchorStrategyCalled = true; return Offset.zero; }, ), ], ), )); final Offset location = tester.getCenter(find.text('Source')); await tester.startGesture(location, pointer: 7); expect(dragAnchorStrategyCalled, true); }); testWidgets('configurable Draggable hit test behavior', (WidgetTester tester) async { const HitTestBehavior hitTestBehavior = HitTestBehavior.deferToChild; await tester.pumpWidget( MaterialApp( home: Column( children: const <Widget>[ Draggable<int>( hitTestBehavior: hitTestBehavior, feedback: SizedBox(height: 50.0, child: Text('Draggable')), child: SizedBox(height: 50.0, child: Text('Target')), ), ], ), ), ); expect(tester.widget<Listener>(find.byType(Listener).first).behavior, hitTestBehavior); }); testWidgets('configurable DragTarget hit test behavior', (WidgetTester tester) async { const HitTestBehavior hitTestBehavior = HitTestBehavior.deferToChild; await tester.pumpWidget( MaterialApp( home: Column( children: <Widget>[ DragTarget<int>( hitTestBehavior: hitTestBehavior, builder: (BuildContext context, List<int?> data, List<dynamic> rejects) { return const SizedBox(height: 100.0, child: Text('Target')); }, ), ], ), ), ); expect(tester.widget<MetaData>(find.byType(MetaData)).behavior, hitTestBehavior); }); testWidgets('LongPressDraggable.dragAnchorStrategy', (WidgetTester tester) async { const Widget widget1 = Placeholder(key: ValueKey<int>(1)); const Widget widget2 = Placeholder(key: ValueKey<int>(2)); Offset dummyStrategy(Draggable<Object> draggable, BuildContext context, Offset position) => Offset.zero; expect(const LongPressDraggable<int>(child: widget1, feedback: widget2), isA<Draggable<int>>()); expect(const LongPressDraggable<int>(child: widget1, feedback: widget2).child, widget1); expect(const LongPressDraggable<int>(child: widget1, feedback: widget2).feedback, widget2); expect(const LongPressDraggable<int>(child: widget1, feedback: widget2, dragAnchor: DragAnchor.child).dragAnchor, DragAnchor.child); expect(const LongPressDraggable<int>(child: widget1, feedback: widget2, dragAnchor: DragAnchor.pointer).dragAnchor, DragAnchor.pointer); expect(LongPressDraggable<int>(child: widget1, feedback: widget2, dragAnchorStrategy: dummyStrategy).dragAnchorStrategy, dummyStrategy); }); } Future<void> _testLongPressDraggableHapticFeedback({ required WidgetTester tester, required bool hapticFeedbackOnStart, required int expectedHapticFeedbackCount }) async { bool onDragStartedCalled = false; int hapticFeedbackCalls = 0; tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.platform, (MethodCall methodCall) async { if (methodCall.method == 'HapticFeedback.vibrate') { hapticFeedbackCalls++; } }); await tester.pumpWidget(MaterialApp( home: LongPressDraggable<int>( data: 1, child: const Text('Source'), feedback: const Text('Dragging'), hapticFeedbackOnStart: hapticFeedbackOnStart, onDragStarted: () { onDragStartedCalled = true; }, ), )); expect(find.text('Source'), findsOneWidget); expect(find.text('Dragging'), findsNothing); expect(onDragStartedCalled, isFalse); final Offset firstLocation = tester.getCenter(find.text('Source')); await tester.startGesture(firstLocation, pointer: 7); await tester.pump(); expect(find.text('Source'), findsOneWidget); expect(find.text('Dragging'), findsNothing); expect(onDragStartedCalled, isFalse); await tester.pump(kLongPressTimeout); expect(find.text('Source'), findsOneWidget); expect(find.text('Dragging'), findsOneWidget); expect(onDragStartedCalled, isTrue); expect(hapticFeedbackCalls, expectedHapticFeedbackCount); } Future<void> _testChildAnchorFeedbackPosition({ required WidgetTester tester, double top = 0.0, double left = 0.0 }) async { final List<int> accepted = <int>[]; final List<DragTargetDetails<int>> acceptedDetails = <DragTargetDetails<int>>[]; int dragStartedCount = 0; await tester.pumpWidget( Stack( textDirection: TextDirection.ltr, children: <Widget>[ Positioned( left: left, top: top, right: 0.0, bottom: 0.0, child: MaterialApp( home: Column( children: <Widget>[ Draggable<int>( data: 1, child: const Text('Source'), feedback: const Text('Dragging'), onDragStarted: () { ++dragStartedCount; }, ), DragTarget<int>( builder: (BuildContext context, List<int?> data, List<dynamic> rejects) { return const SizedBox(height: 100.0, child: Text('Target')); }, onAccept: accepted.add, onAcceptWithDetails: acceptedDetails.add, ), ], ), ), ), ], ), ); expect(accepted, isEmpty); expect(acceptedDetails, isEmpty); expect(find.text('Source'), findsOneWidget); expect(find.text('Dragging'), findsNothing); expect(find.text('Target'), findsOneWidget); expect(dragStartedCount, 0); final Offset firstLocation = tester.getCenter(find.text('Source')); final TestGesture gesture = await tester.startGesture(firstLocation, pointer: 7); await tester.pump(); expect(accepted, isEmpty); expect(acceptedDetails, isEmpty); expect(find.text('Source'), findsOneWidget); expect(find.text('Dragging'), findsOneWidget); expect(find.text('Target'), findsOneWidget); expect(dragStartedCount, 1); final Offset secondLocation = tester.getBottomRight(find.text('Target')); await gesture.moveTo(secondLocation); await tester.pump(); expect(accepted, isEmpty); expect(acceptedDetails, isEmpty); expect(find.text('Source'), findsOneWidget); expect(find.text('Dragging'), findsOneWidget); expect(find.text('Target'), findsOneWidget); expect(dragStartedCount, 1); final Offset feedbackTopLeft = tester.getTopLeft(find.text('Dragging')); final Offset sourceTopLeft = tester.getTopLeft(find.text('Source')); final Offset dragOffset = secondLocation - firstLocation; expect(feedbackTopLeft, equals(sourceTopLeft + dragOffset)); } class DragTargetData { } class ExtendedDragTargetData extends DragTargetData { }