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
This commit is contained in:
parent
0229711ba1
commit
ba5b5e7f6f
@ -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 {
|
||||
|
44
packages/flutter_test/test/finders_test.dart
Normal file
44
packages/flutter_test/test/finders_test.dart
Normal file
@ -0,0 +1,44 @@
|
||||
// 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,
|
||||
);
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user