Skip to content

Commit ba5b5e7

Browse files
authored
only tap on widgets reachable by hit testing (flutter#11767)
* only tap on widgets reachable by hit testing * use FractionalOffset * added tests * check finder finds correct widget * undo unintentional changes * address comments * style fix * add Directionality in test * fix analysis warning
1 parent 0229711 commit ba5b5e7

File tree

6 files changed

+140
-2
lines changed

6 files changed

+140
-2
lines changed

dev/devicelab/lib/tasks/integration_ui.dart

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,14 @@ Future<TaskResult> runEndToEndTests() async {
2121
if (deviceOperatingSystem == DeviceOperatingSystem.ios)
2222
await prepareProvisioningCertificates(testDirectory.path);
2323

24-
await flutter('drive', options: <String>['--verbose', '-d', deviceId, 'lib/keyboard_resize.dart']);
24+
const List<String> entryPoints = const <String>[
25+
'lib/keyboard_resize.dart',
26+
'lib/driver.dart',
27+
];
28+
29+
for (final String entryPoint in entryPoints) {
30+
await flutter('drive', options: <String>['--verbose', '-d', deviceId, entryPoint]);
31+
}
2532
});
2633

2734
return new TaskResult.success(<String, dynamic>{});

dev/integration_tests/ui/lib/driver.dart

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ class DriverTestApp extends StatefulWidget {
2323

2424
class DriverTestAppState extends State<DriverTestApp> {
2525
bool present = true;
26+
Letter _selectedValue = Letter.a;
2627

2728
@override
2829
Widget build(BuildContext context) {
@@ -52,9 +53,41 @@ class DriverTestAppState extends State<DriverTestApp> {
5253
),
5354
],
5455
),
56+
new Row(
57+
children: <Widget>[
58+
const Expanded(
59+
child: const Text('hit testability'),
60+
),
61+
new DropdownButton<Letter>(
62+
key: const ValueKey<String>('dropdown'),
63+
value: _selectedValue,
64+
onChanged: (Letter newValue) {
65+
setState(() {
66+
_selectedValue = newValue;
67+
});
68+
},
69+
items: <DropdownMenuItem<Letter>>[
70+
const DropdownMenuItem<Letter>(
71+
value: Letter.a,
72+
child: const Text('Aaa', key: const ValueKey<String>('a')),
73+
),
74+
const DropdownMenuItem<Letter>(
75+
value: Letter.b,
76+
child: const Text('Bbb', key: const ValueKey<String>('b')),
77+
),
78+
const DropdownMenuItem<Letter>(
79+
value: Letter.c,
80+
child: const Text('Ccc', key: const ValueKey<String>('c')),
81+
),
82+
],
83+
),
84+
],
85+
),
5586
],
5687
),
5788
),
5889
);
5990
}
6091
}
92+
93+
enum Letter { a, b, c }

dev/integration_tests/ui/test_driver/driver_test.dart

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,5 +77,21 @@ void main() {
7777
test('waitForAbsent resolves immediately when the element does not exist', () async {
7878
await driver.waitForAbsent(find.text('that does not exist'));
7979
});
80+
81+
test('uses hit test to determine tappable elements', () async {
82+
final SerializableFinder a = find.byValueKey('a');
83+
final SerializableFinder menu = find.byType('_DropdownMenu<Letter>');
84+
85+
// Dropdown is closed
86+
await driver.waitForAbsent(menu);
87+
88+
// Open dropdown
89+
await driver.tap(a);
90+
await driver.waitFor(menu);
91+
92+
// Close it again
93+
await driver.tap(a);
94+
await driver.waitForAbsent(menu);
95+
});
8096
});
8197
}

packages/flutter_driver/lib/src/extension.dart

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -262,7 +262,10 @@ class FlutterDriverExtension {
262262

263263
Future<TapResult> _tap(Command command) async {
264264
final Tap tapCommand = command;
265-
await _prober.tap(await _waitForElement(_createFinder(tapCommand.finder)));
265+
final Finder computedFinder = await _waitForElement(
266+
_createFinder(tapCommand.finder).hitTestable()
267+
);
268+
await _prober.tap(computedFinder);
266269
return new TapResult();
267270
}
268271

packages/flutter_test/lib/src/finders.dart

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5+
import 'package:flutter/gestures.dart';
56
import 'package:flutter/material.dart';
67
import 'package:meta/meta.dart';
78

@@ -284,6 +285,13 @@ abstract class Finder {
284285
/// matched by this finder.
285286
Finder get last => new _LastFinder(this);
286287

288+
/// Returns a variant of this finder that only matches elements reachable by
289+
/// a hit test.
290+
///
291+
/// The [at] parameter specifies the location relative to the size of the
292+
/// target element where the hit test is performed.
293+
Finder hitTestable({ FractionalOffset at: FractionalOffset.center }) => new _HitTestableFinder(this, at);
294+
287295
@override
288296
String toString() {
289297
final String additional = skipOffstage ? ' (ignoring offstage widgets)' : '';
@@ -327,6 +335,33 @@ class _LastFinder extends Finder {
327335
}
328336
}
329337

338+
class _HitTestableFinder extends Finder {
339+
_HitTestableFinder(this.parent, this.offset);
340+
341+
final Finder parent;
342+
final FractionalOffset offset;
343+
344+
@override
345+
String get description => '${parent.description} (considering only hit-testable ones)';
346+
347+
@override
348+
Iterable<Element> apply(Iterable<Element> candidates) sync* {
349+
for (final Element candidate in parent.apply(candidates)) {
350+
final RenderBox box = candidate.renderObject;
351+
assert(box != null);
352+
final Offset absoluteOffset = box.localToGlobal(offset.alongSize(box.size));
353+
final HitTestResult hitResult = new HitTestResult();
354+
WidgetsBinding.instance.hitTest(hitResult, absoluteOffset);
355+
for (final HitTestEntry entry in hitResult.path) {
356+
if (entry.target == candidate.renderObject) {
357+
yield candidate;
358+
break;
359+
}
360+
}
361+
}
362+
}
363+
}
364+
330365
/// Searches a widget tree and returns nodes that match a particular
331366
/// pattern.
332367
abstract class MatchFinder extends Finder {
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// Copyright 2016 The Chromium Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'package:flutter/material.dart';
6+
import 'package:flutter/widgets.dart';
7+
import 'package:flutter_test/flutter_test.dart';
8+
9+
void main() {
10+
group('hitTestable', () {
11+
testWidgets('excludes non-hit-testable widgets', (WidgetTester tester) async {
12+
await tester.pumpWidget(
13+
_boilerplate(new IndexedStack(
14+
sizing: StackFit.expand,
15+
children: <Widget>[
16+
new GestureDetector(
17+
key: const ValueKey<int>(0),
18+
behavior: HitTestBehavior.opaque,
19+
onTap: () { },
20+
child: const SizedBox.expand(),
21+
),
22+
new GestureDetector(
23+
key: const ValueKey<int>(1),
24+
behavior: HitTestBehavior.opaque,
25+
onTap: () { },
26+
child: const SizedBox.expand(),
27+
),
28+
],
29+
)),
30+
);
31+
expect(find.byType(GestureDetector), findsNWidgets(2));
32+
final Finder hitTestable = find.byType(GestureDetector).hitTestable(at: const FractionalOffset(0.5, 0.5));
33+
expect(hitTestable, findsOneWidget);
34+
expect(tester.widget(hitTestable).key, const ValueKey<int>(0));
35+
});
36+
});
37+
}
38+
39+
Widget _boilerplate(Widget child) {
40+
return new Directionality(
41+
textDirection: TextDirection.ltr,
42+
child: child,
43+
);
44+
}

0 commit comments

Comments
 (0)