// Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'dart:ui'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/semantics.dart'; import 'package:flutter_test/flutter_test.dart'; class TestDragData { const TestDragData( this.slop, this.dragDistance, this.expectedOffsets, ); final Offset slop; final Offset dragDistance; final List<Offset> expectedOffsets; } void main() { group('getSemanticsData', () { testWidgets('throws when there are no semantics', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Scaffold( body: Text('hello'), ), ), ); expect(() => tester.getSemantics(find.text('hello')), throwsStateError); }, semanticsEnabled: false); testWidgets('throws when there are multiple results from the finder', (WidgetTester tester) async { final SemanticsHandle semanticsHandle = tester.ensureSemantics(); await tester.pumpWidget( MaterialApp( home: Scaffold( body: Row( children: const <Widget>[ Text('hello'), Text('hello'), ], ), ), ), ); expect(() => tester.getSemantics(find.text('hello')), throwsStateError); semanticsHandle.dispose(); }); testWidgets('Returns the correct SemanticsData', (WidgetTester tester) async { final SemanticsHandle semanticsHandle = tester.ensureSemantics(); await tester.pumpWidget( MaterialApp( home: Scaffold( body: OutlinedButton( onPressed: () { }, child: const Text('hello'), ), ), ), ); final SemanticsNode node = tester.getSemantics(find.text('hello')); final SemanticsData semantics = node.getSemanticsData(); expect(semantics.label, 'hello'); expect(semantics.hasAction(SemanticsAction.tap), true); expect(semantics.hasFlag(SemanticsFlag.isButton), true); semanticsHandle.dispose(); }); testWidgets('Can enable semantics for tests via semanticsEnabled', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( body: OutlinedButton( onPressed: () { }, child: const Text('hello'), ), ), ), ); final SemanticsNode node = tester.getSemantics(find.text('hello')); final SemanticsData semantics = node.getSemanticsData(); expect(semantics.label, 'hello'); expect(semantics.hasAction(SemanticsAction.tap), true); expect(semantics.hasFlag(SemanticsFlag.isButton), true); }); testWidgets('Returns merged SemanticsData', (WidgetTester tester) async { final SemanticsHandle semanticsHandle = tester.ensureSemantics(); const Key key = Key('test'); await tester.pumpWidget( MaterialApp( home: Scaffold( body: Semantics( label: 'A', child: Semantics( label: 'B', child: Semantics( key: key, label: 'C', child: Container(), ), ), ), ), ), ); final SemanticsNode node = tester.getSemantics(find.byKey(key)); final SemanticsData semantics = node.getSemanticsData(); expect(semantics.label, 'A\nB\nC'); semanticsHandle.dispose(); }); testWidgets('Does not return partial semantics', (WidgetTester tester) async { final SemanticsHandle semanticsHandle = tester.ensureSemantics(); final Key key = UniqueKey(); await tester.pumpWidget( MaterialApp( home: Scaffold( body: MergeSemantics( child: Semantics( container: true, label: 'A', child: Semantics( container: true, key: key, label: 'B', child: Container(), ), ), ), ), ), ); final SemanticsNode node = tester.getSemantics(find.byKey(key)); final SemanticsData semantics = node.getSemanticsData(); expect(semantics.label, 'A\nB'); semanticsHandle.dispose(); }); }); testWidgets( 'WidgetTester.drag must break the offset into multiple parallel components if ' 'the drag goes outside the touch slop values', (WidgetTester tester) async { // This test checks to make sure that the total drag will be correctly split into // pieces such that the first (and potentially second) moveBy function call(s) in // controller.drag() will never have a component greater than the touch // slop in that component's respective axis. const List<TestDragData> offsetResults = <TestDragData>[ TestDragData( Offset(10.0, 10.0), Offset(-150.0, 200.0), <Offset>[ Offset(-7.5, 10.0), Offset(-2.5, 3.333333333333333), Offset(-140.0, 186.66666666666666), ], ), TestDragData( Offset(10.0, 10.0), Offset(150, -200), <Offset>[ Offset(7.5, -10), Offset(2.5, -3.333333333333333), Offset(140.0, -186.66666666666666), ], ), TestDragData( Offset(10.0, 10.0), Offset(-200, 150), <Offset>[ Offset(-10, 7.5), Offset(-3.333333333333333, 2.5), Offset(-186.66666666666666, 140.0), ], ), TestDragData( Offset(10.0, 10.0), Offset(200.0, -150.0), <Offset>[ Offset(10, -7.5), Offset(3.333333333333333, -2.5), Offset(186.66666666666666, -140.0), ], ), TestDragData( Offset(10.0, 10.0), Offset(-150.0, -200.0), <Offset>[ Offset(-7.5, -10.0), Offset(-2.5, -3.333333333333333), Offset(-140.0, -186.66666666666666), ], ), TestDragData( Offset(10.0, 10.0), Offset(8.0, 3.0), <Offset>[ Offset(8.0, 3.0), ], ), TestDragData( Offset(10.0, 10.0), Offset(3.0, 8.0), <Offset>[ Offset(3.0, 8.0), ], ), TestDragData( Offset(10.0, 10.0), Offset(20.0, 5.0), <Offset>[ Offset(10.0, 2.5), Offset(10.0, 2.5), ], ), TestDragData( Offset(10.0, 10.0), Offset(5.0, 20.0), <Offset>[ Offset(2.5, 10.0), Offset(2.5, 10.0), ], ), TestDragData( Offset(10.0, 10.0), Offset(20.0, 15.0), <Offset>[ Offset(10.0, 7.5), Offset(3.333333333333333, 2.5), Offset(6.666666666666668, 5.0), ], ), TestDragData( Offset(10.0, 10.0), Offset(15.0, 20.0), <Offset>[ Offset(7.5, 10.0), Offset(2.5, 3.333333333333333), Offset(5.0, 6.666666666666668), ], ), TestDragData( Offset(10.0, 10.0), Offset(20.0, 20.0), <Offset>[ Offset(10.0, 10.0), Offset(10.0, 10.0), ], ), TestDragData( Offset(10.0, 10.0), Offset(0.0, 5.0), <Offset>[ Offset(0.0, 5.0), ], ), //// Varying touch slops TestDragData( Offset(12.0, 5.0), Offset(0.0, 5.0), <Offset>[ Offset(0.0, 5.0), ], ), TestDragData( Offset(12.0, 5.0), Offset(20.0, 5.0), <Offset>[ Offset(12.0, 3.0), Offset(8.0, 2.0), ], ), TestDragData( Offset(12.0, 5.0), Offset(5.0, 20.0), <Offset>[ Offset(1.25, 5.0), Offset(3.75, 15.0), ], ), TestDragData( Offset(5.0, 12.0), Offset(5.0, 20.0), <Offset>[ Offset(3.0, 12.0), Offset(2.0, 8.0), ], ), TestDragData( Offset(5.0, 12.0), Offset(20.0, 5.0), <Offset>[ Offset(5.0, 1.25), Offset(15.0, 3.75), ], ), TestDragData( Offset(18.0, 18.0), Offset(0.0, 150.0), <Offset>[ Offset(0.0, 18.0), Offset(0.0, 132.0), ], ), TestDragData( Offset(18.0, 18.0), Offset(0.0, -150.0), <Offset>[ Offset(0.0, -18.0), Offset(0.0, -132.0), ], ), TestDragData( Offset(18.0, 18.0), Offset(-150.0, 0.0), <Offset>[ Offset(-18.0, 0.0), Offset(-132.0, 0.0), ], ), TestDragData( Offset.zero, Offset(-150.0, 0.0), <Offset>[ Offset(-150.0, 0.0), ], ), TestDragData( Offset(18.0, 18.0), Offset(-32.0, 0.0), <Offset>[ Offset(-18.0, 0.0), Offset(-14.0, 0.0), ], ), ]; final List<Offset> dragOffsets = <Offset>[]; await tester.pumpWidget( Listener( onPointerMove: (PointerMoveEvent event) { dragOffsets.add(event.delta); }, child: const Text('test', textDirection: TextDirection.ltr), ), ); for (int resultIndex = 0; resultIndex < offsetResults.length; resultIndex += 1) { final TestDragData testResult = offsetResults[resultIndex]; await tester.drag( find.text('test'), testResult.dragDistance, touchSlopX: testResult.slop.dx, touchSlopY: testResult.slop.dy, ); expect( testResult.expectedOffsets.length, dragOffsets.length, reason: 'There is a difference in the number of expected and actual split offsets for the drag with:\n' 'Touch Slop: ${testResult.slop}\n' 'Delta: ${testResult.dragDistance}\n', ); for (int valueIndex = 0; valueIndex < offsetResults[resultIndex].expectedOffsets.length; valueIndex += 1) { expect( testResult.expectedOffsets[valueIndex], offsetMoreOrLessEquals(dragOffsets[valueIndex]), reason: 'There is a difference in the expected and actual value of the ' '${valueIndex == 2 ? 'first' : valueIndex == 3 ? 'second' : 'third'}' ' split offset for the drag with:\n' 'Touch slop: ${testResult.slop}\n' 'Delta: ${testResult.dragDistance}\n' ); } dragOffsets.clear(); } }, ); testWidgets( 'WidgetTester.tap must respect buttons', (WidgetTester tester) async { final List<String> logs = <String>[]; await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Listener( onPointerDown: (PointerDownEvent event) => logs.add('down ${event.buttons}'), onPointerMove: (PointerMoveEvent event) => logs.add('move ${event.buttons}'), onPointerUp: (PointerUpEvent event) => logs.add('up ${event.buttons}'), child: const Text('test'), ), ), ); await tester.tap(find.text('test'), buttons: kSecondaryMouseButton); const String b = '$kSecondaryMouseButton'; for(int i = 0; i < logs.length; i++) { if (i == 0) expect(logs[i], 'down $b'); else if (i != logs.length - 1) expect(logs[i], 'move $b'); else expect(logs[i], 'up 0'); } }, ); testWidgets( 'WidgetTester.press must respect buttons', (WidgetTester tester) async { final List<String> logs = <String>[]; await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Listener( onPointerDown: (PointerDownEvent event) => logs.add('down ${event.buttons}'), onPointerMove: (PointerMoveEvent event) => logs.add('move ${event.buttons}'), onPointerUp: (PointerUpEvent event) => logs.add('up ${event.buttons}'), child: const Text('test'), ), ), ); await tester.press(find.text('test'), buttons: kSecondaryMouseButton); const String b = '$kSecondaryMouseButton'; expect(logs, equals(<String>['down $b'])); }, ); testWidgets( 'WidgetTester.longPress must respect buttons', (WidgetTester tester) async { final List<String> logs = <String>[]; await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Listener( onPointerDown: (PointerDownEvent event) => logs.add('down ${event.buttons}'), onPointerMove: (PointerMoveEvent event) => logs.add('move ${event.buttons}'), onPointerUp: (PointerUpEvent event) => logs.add('up ${event.buttons}'), child: const Text('test'), ), ), ); await tester.longPress(find.text('test'), buttons: kSecondaryMouseButton); await tester.pumpAndSettle(); const String b = '$kSecondaryMouseButton'; for(int i = 0; i < logs.length; i++) { if (i == 0) expect(logs[i], 'down $b'); else if (i != logs.length - 1) expect(logs[i], 'move $b'); else expect(logs[i], 'up 0'); } }, ); testWidgets( 'WidgetTester.drag must respect buttons', (WidgetTester tester) async { final List<String> logs = <String>[]; await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Listener( onPointerDown: (PointerDownEvent event) => logs.add('down ${event.buttons}'), onPointerMove: (PointerMoveEvent event) => logs.add('move ${event.buttons}'), onPointerUp: (PointerUpEvent event) => logs.add('up ${event.buttons}'), child: const Text('test'), ), ), ); await tester.drag(find.text('test'), const Offset(-150.0, 200.0), buttons: kSecondaryMouseButton); const String b = '$kSecondaryMouseButton'; for(int i = 0; i < logs.length; i++) { if (i == 0) expect(logs[i], 'down $b'); else if (i != logs.length - 1) expect(logs[i], 'move $b'); else expect(logs[i], 'up 0'); } }, ); testWidgets( 'WidgetTester.fling must respect buttons', (WidgetTester tester) async { final List<String> logs = <String>[]; await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Listener( onPointerDown: (PointerDownEvent event) => logs.add('down ${event.buttons}'), onPointerMove: (PointerMoveEvent event) => logs.add('move ${event.buttons}'), onPointerUp: (PointerUpEvent event) => logs.add('up ${event.buttons}'), child: const Text('test'), ), ), ); await tester.fling(find.text('test'), const Offset(-10.0, 0.0), 1000.0, buttons: kSecondaryMouseButton); await tester.pumpAndSettle(); const String b = '$kSecondaryMouseButton'; for(int i = 0; i < logs.length; i++) { if (i == 0) expect(logs[i], 'down $b'); else if (i != logs.length - 1) expect(logs[i], 'move $b'); else expect(logs[i], 'up 0'); } }, ); testWidgets( 'WidgetTester.fling produces strictly monotonically increasing timestamps, ' 'when given a large velocity', (WidgetTester tester) async { // Velocity trackers may misbehave if the `PointerMoveEvent`s' have the // same timestamp. This is more likely to happen when the velocity tracker // has a small sample size. final List<Duration> logs = <Duration>[]; await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Listener( onPointerMove: (PointerMoveEvent event) => logs.add(event.timeStamp), child: const Text('test'), ), ), ); await tester.fling(find.text('test'), const Offset(0.0, -50.0), 10000.0); await tester.pumpAndSettle(); for (int i = 0; i + 1 < logs.length; i += 1) { expect(logs[i + 1], greaterThan(logs[i])); } }); testWidgets( 'WidgetTester.timedDrag must respect buttons', (WidgetTester tester) async { final List<String> logs = <String>[]; await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Listener( onPointerDown: (PointerDownEvent event) => logs.add('down ${event.buttons}'), onPointerMove: (PointerMoveEvent event) => logs.add('move ${event.buttons}'), onPointerUp: (PointerUpEvent event) => logs.add('up ${event.buttons}'), child: const Text('test'), ), ), ); await tester.timedDrag( find.text('test'), const Offset(-200.0, 0.0), const Duration(seconds: 1), buttons: kSecondaryMouseButton, ); await tester.pumpAndSettle(); const String b = '$kSecondaryMouseButton'; for(int i = 0; i < logs.length; i++) { if (i == 0) expect(logs[i], 'down $b'); else if (i != logs.length - 1) expect(logs[i], 'move $b'); else expect(logs[i], 'up 0'); } }, ); testWidgets( 'WidgetTester.timedDrag uses correct pointer', (WidgetTester tester) async { final List<String> logs = <String>[]; await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Listener( onPointerDown: (PointerDownEvent event) => logs.add('down ${event.pointer}'), child: const Text('test'), ), ), ); await tester.timedDrag( find.text('test'), const Offset(-200.0, 0.0), const Duration(seconds: 1), buttons: kSecondaryMouseButton, ); await tester.pumpAndSettle(); await tester.timedDrag( find.text('test'), const Offset(200.0, 0.0), const Duration(seconds: 1), buttons: kSecondaryMouseButton, ); await tester.pumpAndSettle(); expect(logs.length, 2); expect(logs[0], isNotNull); expect(logs[1], isNotNull); expect(logs[1] != logs[0], isTrue); }, ); testWidgets( 'ensureVisible: scrolls to make widget visible', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( body: ListView.builder( itemCount: 20, shrinkWrap: true, itemBuilder: (BuildContext context, int i) => ListTile(title: Text('Item $i')), ), ), ), ); // Make sure widget isn't on screen expect(find.text('Item 15'), findsNothing); await tester.ensureVisible(find.text('Item 15', skipOffstage: false)); await tester.pumpAndSettle(); expect(find.text('Item 15'), findsOneWidget); }, ); group('scrollUntilVisible: scrolls to make unbuilt widget visible', () { testWidgets( 'Vertical', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( body: ListView.builder( itemCount: 50, shrinkWrap: true, itemBuilder: (BuildContext context, int i) => ListTile(title: Text('Item $i')), ), ), ), ); // Make sure widget isn't built yet. expect(find.text('Item 45', skipOffstage: false), findsNothing); await tester.scrollUntilVisible( find.text('Item 45', skipOffstage: false), 100, ); await tester.pumpAndSettle(); // Now the widget is on screen. expect(find.text('Item 45'), findsOneWidget); }, ); testWidgets( 'Horizontal', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( body: ListView.builder( itemCount: 50, shrinkWrap: true, scrollDirection: Axis.horizontal, // ListTile does not support horizontal list itemBuilder: (BuildContext context, int i) => Text('Item $i'), ), ), ), ); // Make sure widget isn't built yet. expect(find.text('Item 45', skipOffstage: false), findsNothing); await tester.scrollUntilVisible( find.text('Item 45', skipOffstage: false), 100, ); await tester.pumpAndSettle(); // Now the widget is on screen. expect(find.text('Item 45'), findsOneWidget); }, ); testWidgets( 'Fail', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( body: ListView.builder( itemCount: 50, shrinkWrap: true, itemBuilder: (BuildContext context, int i) => ListTile(title: Text('Item $i')), ), ), ), ); try { await tester.scrollUntilVisible( find.text('Item 55', skipOffstage: false), 100, ); } on StateError catch (e) { expect(e.message, 'No element'); } }, ); testWidgets('Drag Until Visible', (WidgetTester tester) async { // when there are two implicit [Scrollable], `scrollUntilVisible` is hard // to use. await tester.pumpWidget( MaterialApp( home: Scaffold( body: Column( children: <Widget>[ SizedBox(height: 200, child: ListView.builder( key: const Key('listView-a'), itemCount: 50, shrinkWrap: true, itemBuilder: (BuildContext context, int i) => ListTile(title: Text('Item a-$i')), )), const Divider(thickness: 5), Expanded(child: ListView.builder( key: const Key('listView-b'), itemCount: 50, shrinkWrap: true, itemBuilder: (BuildContext context, int i) => ListTile(title: Text('Item b-$i')), )), ], ), ), ), ); await tester.pumpAndSettle(); expect(find.byType(Scrollable), findsNWidgets(2)); // Make sure widget isn't built yet. expect(find.text('Item b-45', skipOffstage: false), findsNothing); await tester.dragUntilVisible( find.text('Item b-45', skipOffstage: false), find.byKey(const ValueKey<String>('listView-b')), const Offset(0, -100), ); await tester.pumpAndSettle(); // Now the widget is on screen. expect(find.text('Item b-45'), findsOneWidget); }); }); }