blob: 3513e9b4262bfecf34799da60b8bb24515b5befc [file] [log] [blame]
// 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);
});
});
}