Commit ba5b5e7f authored by Yegor's avatar Yegor Committed by GitHub

only tap on widgets reachable by hit testing (#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
parent 0229711b
......@@ -21,7 +21,14 @@ Future<TaskResult> runEndToEndTests() async {
if (deviceOperatingSystem == DeviceOperatingSystem.ios)
await prepareProvisioningCertificates(testDirectory.path);
await flutter('drive', options: <String>['--verbose', '-d', deviceId, 'lib/keyboard_resize.dart']);
const List<String> entryPoints = const <String>[
'lib/keyboard_resize.dart',
'lib/driver.dart',
];
for (final String entryPoint in entryPoints) {
await flutter('drive', options: <String>['--verbose', '-d', deviceId, entryPoint]);
}
});
return new TaskResult.success(<String, dynamic>{});
......
......@@ -23,6 +23,7 @@ class DriverTestApp extends StatefulWidget {
class DriverTestAppState extends State<DriverTestApp> {
bool present = true;
Letter _selectedValue = Letter.a;
@override
Widget build(BuildContext context) {
......@@ -52,9 +53,41 @@ class DriverTestAppState extends State<DriverTestApp> {
),
],
),
new Row(
children: <Widget>[
const Expanded(
child: const Text('hit testability'),
),
new DropdownButton<Letter>(
key: const ValueKey<String>('dropdown'),
value: _selectedValue,
onChanged: (Letter newValue) {
setState(() {
_selectedValue = newValue;
});
},
items: <DropdownMenuItem<Letter>>[
const DropdownMenuItem<Letter>(
value: Letter.a,
child: const Text('Aaa', key: const ValueKey<String>('a')),
),
const DropdownMenuItem<Letter>(
value: Letter.b,
child: const Text('Bbb', key: const ValueKey<String>('b')),
),
const DropdownMenuItem<Letter>(
value: Letter.c,
child: const Text('Ccc', key: const ValueKey<String>('c')),
),
],
),
],
),
],
),
),
);
}
}
enum Letter { a, b, c }
......@@ -77,5 +77,21 @@ void main() {
test('waitForAbsent resolves immediately when the element does not exist', () async {
await driver.waitForAbsent(find.text('that does not exist'));
});
test('uses hit test to determine tappable elements', () async {
final SerializableFinder a = find.byValueKey('a');
final SerializableFinder menu = find.byType('_DropdownMenu<Letter>');
// Dropdown is closed
await driver.waitForAbsent(menu);
// Open dropdown
await driver.tap(a);
await driver.waitFor(menu);
// Close it again
await driver.tap(a);
await driver.waitForAbsent(menu);
});
});
}
......@@ -262,7 +262,10 @@ class FlutterDriverExtension {
Future<TapResult> _tap(Command command) async {
final Tap tapCommand = command;
await _prober.tap(await _waitForElement(_createFinder(tapCommand.finder)));
final Finder computedFinder = await _waitForElement(
_createFinder(tapCommand.finder).hitTestable()
);
await _prober.tap(computedFinder);
return new TapResult();
}
......
......@@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:meta/meta.dart';
......@@ -284,6 +285,13 @@ abstract class Finder {
/// matched by this finder.
Finder get last => new _LastFinder(this);
/// Returns a variant of this finder that only matches elements reachable by
/// a hit test.
///
/// The [at] parameter specifies the location relative to the size of the
/// target element where the hit test is performed.
Finder hitTestable({ FractionalOffset at: FractionalOffset.center }) => new _HitTestableFinder(this, at);
@override
String toString() {
final String additional = skipOffstage ? ' (ignoring offstage widgets)' : '';
......@@ -327,6 +335,33 @@ class _LastFinder extends Finder {
}
}
class _HitTestableFinder extends Finder {
_HitTestableFinder(this.parent, this.offset);
final Finder parent;
final FractionalOffset offset;
@override
String get description => '${parent.description} (considering only hit-testable ones)';
@override
Iterable<Element> apply(Iterable<Element> candidates) sync* {
for (final Element candidate in parent.apply(candidates)) {
final RenderBox box = candidate.renderObject;
assert(box != null);
final Offset absoluteOffset = box.localToGlobal(offset.alongSize(box.size));
final HitTestResult hitResult = new HitTestResult();
WidgetsBinding.instance.hitTest(hitResult, absoluteOffset);
for (final HitTestEntry entry in hitResult.path) {
if (entry.target == candidate.renderObject) {
yield candidate;
break;
}
}
}
}
}
/// Searches a widget tree and returns nodes that match a particular
/// pattern.
abstract class MatchFinder extends Finder {
......
// Copyright 2016 The Chromium 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 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group('hitTestable', () {
testWidgets('excludes non-hit-testable widgets', (WidgetTester tester) async {
await tester.pumpWidget(
_boilerplate(new IndexedStack(
sizing: StackFit.expand,
children: <Widget>[
new GestureDetector(
key: const ValueKey<int>(0),
behavior: HitTestBehavior.opaque,
onTap: () { },
child: const SizedBox.expand(),
),
new GestureDetector(
key: const ValueKey<int>(1),
behavior: HitTestBehavior.opaque,
onTap: () { },
child: const SizedBox.expand(),
),
],
)),
);
expect(find.byType(GestureDetector), findsNWidgets(2));
final Finder hitTestable = find.byType(GestureDetector).hitTestable(at: const FractionalOffset(0.5, 0.5));
expect(hitTestable, findsOneWidget);
expect(tester.widget(hitTestable).key, const ValueKey<int>(0));
});
});
}
Widget _boilerplate(Widget child) {
return new Directionality(
textDirection: TextDirection.ltr,
child: child,
);
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment