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)
|
if (deviceOperatingSystem == DeviceOperatingSystem.ios)
|
||||||
await prepareProvisioningCertificates(testDirectory.path);
|
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>{});
|
return new TaskResult.success(<String, dynamic>{});
|
||||||
|
@ -23,6 +23,7 @@ class DriverTestApp extends StatefulWidget {
|
|||||||
|
|
||||||
class DriverTestAppState extends State<DriverTestApp> {
|
class DriverTestAppState extends State<DriverTestApp> {
|
||||||
bool present = true;
|
bool present = true;
|
||||||
|
Letter _selectedValue = Letter.a;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
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 {
|
test('waitForAbsent resolves immediately when the element does not exist', () async {
|
||||||
await driver.waitForAbsent(find.text('that does not exist'));
|
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 {
|
Future<TapResult> _tap(Command command) async {
|
||||||
final Tap tapCommand = command;
|
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();
|
return new TapResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
// Use of this source code is governed by a BSD-style license that can be
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
// found in the LICENSE file.
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
import 'package:flutter/gestures.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:meta/meta.dart';
|
import 'package:meta/meta.dart';
|
||||||
|
|
||||||
@ -284,6 +285,13 @@ abstract class Finder {
|
|||||||
/// matched by this finder.
|
/// matched by this finder.
|
||||||
Finder get last => new _LastFinder(this);
|
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
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
final String additional = skipOffstage ? ' (ignoring offstage widgets)' : '';
|
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
|
/// Searches a widget tree and returns nodes that match a particular
|
||||||
/// pattern.
|
/// pattern.
|
||||||
abstract class MatchFinder extends Finder {
|
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