Commit 06d80f22 authored by Ian Hickson's avatar Ian Hickson

Identify the widgets you tap on in live tests (#4079)

parent 94636bd2
// 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_test/flutter_test.dart';
import 'package:flutter_gallery/main.dart' as flutter_gallery_main;
void main() {
TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.ensureInitialized();
if (binding is LiveTestWidgetsFlutterBinding)
binding.allowAllFrames = true;
testWidgets('Flutter Gallery app simple smoke test', (WidgetTester tester) async {
flutter_gallery_main.main(); // builds the app and schedules a frame but doesn't trigger one
await tester.pump(); // see https://github.com/flutter/flutter/issues/1865
await tester.pump(); // triggers a frame
Finder finder = find.byWidgetPredicate((Widget widget) {
return widget is Tooltip && widget.message == 'Open navigation menu';
});
expect(finder, findsOneWidget);
// Open drawer
await tester.tap(finder);
await tester.pump(); // start animation
await tester.pump(const Duration(seconds: 1)); // end animation
// Change theme
await tester.tap(find.text('Dark'));
await tester.pump(); // start animation
await tester.pump(const Duration(seconds: 1)); // end animation
// Close drawer
await tester.tap(find.byType(DrawerController));
await tester.pump(); // start animation
await tester.pump(const Duration(seconds: 1)); // end animation
// Open Demos
await tester.tap(find.text('Demos'));
await tester.pump(); // start animation
await tester.pump(const Duration(seconds: 1)); // end animation
// Open Flexible space toolbar
await tester.tap(find.text('Flexible space toolbar'));
await tester.pump(); // start animation
await tester.pump(const Duration(seconds: 1)); // end animation
// Scroll it up
await tester.scroll(find.text('(650) 555-1234'), const Offset(0.0, -50.0));
await tester.pump(const Duration(milliseconds: 200));
await tester.scroll(find.text('(650) 555-1234'), const Offset(0.0, -50.0));
await tester.pump(const Duration(milliseconds: 200));
await tester.scroll(find.text('(650) 555-1234'), const Offset(0.0, -50.0));
await tester.pump(const Duration(milliseconds: 200));
await tester.scroll(find.text('(650) 555-1234'), const Offset(0.0, -50.0));
await tester.pump(const Duration(milliseconds: 200));
await tester.scroll(find.text('(650) 555-1234'), const Offset(0.0, -50.0));
await tester.pump(const Duration(milliseconds: 200));
await tester.scroll(find.text('(650) 555-1234'), const Offset(0.0, -50.0));
await tester.pump(const Duration(milliseconds: 200));
await tester.pump(const Duration(hours: 100)); // for testing
});
}
......@@ -71,7 +71,7 @@ class RenderView extends RenderObject with RenderObjectWithChildMixin<RenderBox>
if (configuration == value)
return;
_configuration = value;
replaceRootLayer(new TransformLayer(transform: _configuration.toMatrix()));
replaceRootLayer(new TransformLayer(transform: configuration.toMatrix()));
markNeedsLayout();
}
......@@ -79,7 +79,7 @@ class RenderView extends RenderObject with RenderObjectWithChildMixin<RenderBox>
void scheduleInitialFrame() {
assert(owner != null);
scheduleInitialLayout();
scheduleInitialPaint(new TransformLayer(transform: _configuration.toMatrix()));
scheduleInitialPaint(new TransformLayer(transform: configuration.toMatrix()));
owner.requestVisualUpdate();
}
......
......@@ -148,6 +148,16 @@ abstract class TestWidgetsFlutterBinding extends BindingBase
return new Future<Null>.value();
}
/// Convert the given point from the global coodinate system (as used by
/// pointer events from the device) to the coordinate system used by the
/// tests (an 800 by 600 window).
Point globalToLocal(Point point) => point;
/// Convert the given point from the coordinate system used by the tests (an
/// 800 by 600 window) to the global coodinate system (as used by pointer
/// events from the device).
Point localToGlobal(Point point) => point;
@override
void dispatchEvent(PointerEvent event, HitTestResult result, {
TestBindingEventSource source: TestBindingEventSource.device
......@@ -595,11 +605,21 @@ class LiveTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding {
@override
_LiveTestRenderView get renderView => super.renderView;
/// An object to which real device events should be routed.
///
/// Normally, device events are silently dropped. However, if this property is
/// set to a non-null value, then the events will be routed to its
/// [HitTestDispatcher.dispatchEvent] method instead.
///
/// Events dispatched by [TestGesture] are not affected by this.
HitTestDispatcher deviceEventDispatcher;
@override
void dispatchEvent(PointerEvent event, HitTestResult result, {
TestBindingEventSource source: TestBindingEventSource.device
}) {
if (source == TestBindingEventSource.test) {
switch (source) {
case TestBindingEventSource.test:
if (!renderView._pointers.containsKey(event.pointer)) {
assert(event.down);
renderView._pointers[event.pointer] = new _LiveTestPointerRecord(event.pointer, event.position);
......@@ -610,10 +630,12 @@ class LiveTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding {
}
renderView.markNeedsPaint();
super.dispatchEvent(event, result, source: source);
return;
break;
case TestBindingEventSource.device:
if (deviceEventDispatcher != null)
deviceEventDispatcher.dispatchEvent(event, result);
break;
}
// we eat all device events for now
// TODO(ianh): do something useful with device events
}
@override
......@@ -654,8 +676,31 @@ class LiveTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding {
@override
ViewConfiguration createViewConfiguration() {
final double actualWidth = ui.window.size.width * ui.window.devicePixelRatio;
final double actualHeight = ui.window.size.height * ui.window.devicePixelRatio;
return new _TestViewConfiguration(
// TODO(ianh): that these are not the same is https://github.com/flutter/flutter/issues/1360
_getMatrix(ui.window.devicePixelRatio),
_getMatrix(1.0)
);
}
@override
Point globalToLocal(Point point) {
Matrix4 transform = renderView.configuration.toHitTestMatrix();
double det = transform.invert();
assert(det != 0.0);
Point result = MatrixUtils.transformPoint(transform, point);
return result;
}
@override
Point localToGlobal(Point point) {
Matrix4 transform = renderView.configuration.toHitTestMatrix();
return MatrixUtils.transformPoint(transform, point);
}
Matrix4 _getMatrix(double devicePixelRatio) {
final double actualWidth = ui.window.size.width * devicePixelRatio;
final double actualHeight = ui.window.size.height * devicePixelRatio;
final double desiredWidth = _kTestViewportSize.width;
final double desiredHeight = _kTestViewportSize.height;
double scale, shiftX, shiftY;
......@@ -671,19 +716,22 @@ class LiveTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding {
final Matrix4 matrix = new Matrix4.compose(
new Vector3(shiftX, shiftY, 0.0), // translation
new Quaternion.identity(), // rotation
new Vector3(scale, scale, 0.0) // scale
new Vector3(scale, scale, 1.0) // scale
);
return new _TestViewConfiguration(matrix);
return matrix;
}
}
class _TestViewConfiguration extends ViewConfiguration {
_TestViewConfiguration(this.matrix) : super(size: _kTestViewportSize);
_TestViewConfiguration(this.paintMatrix, this.hitTestMatrix) : super(size: _kTestViewportSize);
final Matrix4 matrix;
final Matrix4 paintMatrix;
final Matrix4 hitTestMatrix;
@override
Matrix4 toMatrix() => matrix;
Matrix4 toMatrix() => paintMatrix.clone();
Matrix4 toHitTestMatrix() => hitTestMatrix.clone();
@override
String toString() => 'TestViewConfiguration';
......@@ -709,8 +757,20 @@ class _LiveTestRenderView extends RenderView {
ViewConfiguration configuration
}) : super(configuration: configuration);
@override
_TestViewConfiguration get configuration => super.configuration;
final Map<int, _LiveTestPointerRecord> _pointers = <int, _LiveTestPointerRecord>{};
@override
bool hitTest(HitTestResult result, { Point position }) {
Matrix4 transform = configuration.toHitTestMatrix();
double det = transform.invert();
assert(det != 0.0);
position = MatrixUtils.transformPoint(transform, position);
return super.hitTest(result, position: position);
}
@override
void paint(PaintingContext context, Offset offset) {
assert(offset == Offset.zero);
......
......@@ -273,7 +273,7 @@ class WidgetController {
assert(offset.distance > 0.0);
assert(velocity != 0.0); // velocity is pixels/second
final TestPointer p = new TestPointer(pointer);
final HitTestResult result = _hitTest(startLocation);
final HitTestResult result = hitTestOnBinding(startLocation);
const int kMoveCount = 50; // Needs to be >= kHistorySize, see _LeastSquaresVelocityTrackerStrategy
final double timeStampDelta = 1000.0 * offset.distance / (kMoveCount * velocity);
double timeStamp = 0.0;
......@@ -311,10 +311,11 @@ class WidgetController {
/// Begins a gesture at a particular point, and returns the
/// [TestGesture] object which you can use to continue the gesture.
Future<TestGesture> startGesture(Point downLocation, { int pointer: 1 }) {
return TestGesture.down(downLocation, pointer: pointer, dispatcher: sendEventToBinding);
return TestGesture.down(downLocation, pointer: pointer, hitTester: hitTestOnBinding, dispatcher: sendEventToBinding);
}
HitTestResult _hitTest(Point location) {
/// Forwards the given location to the binding's hitTest logic.
HitTestResult hitTestOnBinding(Point location) {
final HitTestResult result = new HitTestResult();
binding.hitTest(result, location);
return result;
......
......@@ -107,9 +107,12 @@ class TestPointer {
}
}
/// An callback that can dispatch events and returns a future that
/// Signature for a callback that can dispatch events and returns a future that
/// completes when the event dispatch is complete.
typedef Future<Null> AsyncHitTestDispatcher(PointerEvent event, HitTestResult result);
typedef Future<Null> EventDispatcher(PointerEvent event, HitTestResult result);
/// Signature for callbacks that perform hit-testing at a given location.
typedef HitTestResult HitTester(Point location);
/// A class for performing gestures in tests.
///
......@@ -124,28 +127,21 @@ class TestGesture {
/// By default, the pointer ID used is 1. This can be overridden by
/// providing the `pointer` argument.
///
/// By default, the global binding is used for hit testing. The
/// object to use for hit testing can be overridden by providing
/// `hitTestTarget`.
///
/// An object to use for dispatching events must be provided via the
/// `dispatcher` argument.
/// A function to use for hit testing should be provided via the `hitTester`
/// argument, and a function to use for dispatching events should be provided
/// via the `dispatcher` argument.
static Future<TestGesture> down(Point downLocation, {
int pointer: 1,
HitTestable target,
AsyncHitTestDispatcher dispatcher
HitTester hitTester,
EventDispatcher dispatcher
}) async {
assert(hitTester != null);
assert(dispatcher != null);
final Completer<TestGesture> completer = new Completer<TestGesture>();
TestGesture result;
TestAsyncUtils.guard(() async {
// hit test
final HitTestResult hitTestResult = new HitTestResult();
target ??= GestureBinding.instance;
assert(target != null);
target.hitTest(hitTestResult, downLocation);
// dispatch down event
final HitTestResult hitTestResult = hitTester(downLocation);
final TestPointer testPointer = new TestPointer(pointer);
await dispatcher(testPointer.down(downLocation), hitTestResult);
......@@ -158,7 +154,7 @@ class TestGesture {
return completer.future;
}
final AsyncHitTestDispatcher _dispatcher;
final EventDispatcher _dispatcher;
final HitTestResult _result;
final TestPointer _pointer;
......
......@@ -5,9 +5,11 @@
import 'dart:async';
import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'package:test/test.dart' as test_package;
import 'all_elements.dart';
import 'binding.dart';
import 'controller.dart';
import 'finders.dart';
......@@ -123,8 +125,11 @@ void expectSync(dynamic actual, dynamic matcher, {
}
/// Class that programmatically interacts with widgets and the test environment.
class WidgetTester extends WidgetController {
WidgetTester._(TestWidgetsFlutterBinding binding) : super(binding);
class WidgetTester extends WidgetController implements HitTestDispatcher {
WidgetTester._(TestWidgetsFlutterBinding binding) : super(binding) {
if (binding is LiveTestWidgetsFlutterBinding)
binding.deviceEventDispatcher = this;
}
/// The binding instance used by the testing framework.
@override
......@@ -157,6 +162,12 @@ class WidgetTester extends WidgetController {
return TestAsyncUtils.guard(() => binding.pump(duration, phase));
}
@override
HitTestResult hitTestOnBinding(Point location) {
location = binding.localToGlobal(location);
return super.hitTestOnBinding(location);
}
@override
Future<Null> sendEventToBinding(PointerEvent event, HitTestResult result) {
return TestAsyncUtils.guard(() async {
......@@ -165,6 +176,103 @@ class WidgetTester extends WidgetController {
});
}
/// Handler for device events caught by the binding in live test mode.
@override
void dispatchEvent(PointerEvent event, HitTestResult result) {
if (event is PointerDownEvent) {
final RenderObject innerTarget = result.path.firstWhere(
(HitTestEntry candidate) => candidate.target is RenderObject,
orElse: () => null
)?.target;
if (innerTarget == null)
return null;
final Element innerTargetElement = collectAllElementsFrom(binding.renderViewElement)
.lastWhere((Element element) => element.renderObject == innerTarget);
final List<Element> candidates = <Element>[];
innerTargetElement.visitAncestorElements((Element element) {
candidates.add(element);
return true;
});
assert(candidates.isNotEmpty);
String descendantText;
int numberOfWithTexts = 0;
int numberOfTypes = 0;
int totalNumber = 0;
print('Some possible finders for the widgets at ${binding.globalToLocal(event.position)}:');
for (Element element in candidates) {
if (totalNumber > 10)
break;
totalNumber += 1;
if (element.widget is Text) {
assert(descendantText == null);
final Text widget = element.widget;
final Iterable<Element> matches = find.text(widget.data).evaluate();
descendantText = widget.data;
if (matches.length == 1) {
print(' find.text(\'${widget.data}\')');
continue;
}
}
if (element.widget.key is ValueKey<dynamic>) {
final ValueKey<dynamic> key = element.widget.key;
String keyLabel;
if ((key is ValueKey<int> ||
key is ValueKey<double> ||
key is ValueKey<bool>)) {
keyLabel = 'const ${element.widget.key.runtimeType}(${key.value})';
} else if (key is ValueKey<String>) {
keyLabel = 'const ${element.widget.key.runtimeType}(\'${key.value}\')';
}
if (keyLabel != null) {
final Iterable<Element> matches = find.byKey(key).evaluate();
if (matches.length == 1) {
print(' find.byKey($keyLabel)');
continue;
}
}
}
if (!_isPrivate(element.widget.runtimeType)) {
if (numberOfTypes < 5) {
final Iterable<Element> matches = find.byType(element.widget.runtimeType).evaluate();
if (matches.length == 1) {
print(' find.byType(${element.widget.runtimeType})');
numberOfTypes += 1;
continue;
}
}
if (descendantText != null && numberOfWithTexts < 5) {
final Iterable<Element> matches = find.widgetWithText(element.widget.runtimeType, descendantText).evaluate();
if (matches.length == 1) {
print(' find.widgetWithText(${element.widget.runtimeType}, \'$descendantText\')');
numberOfWithTexts += 1;
continue;
}
}
}
if (!_isPrivate(element.runtimeType)) {
final Iterable<Element> matches = find.byElementType(element.runtimeType).evaluate();
if (matches.length == 1) {
print(' find.byElementType(${element.runtimeType})');
continue;
}
}
totalNumber -= 1; // if we got here, we didn't actually find something to say about it
}
if (totalNumber == 0)
print(' <could not come up with any unique finders>');
}
}
bool _isPrivate(Type type) {
return '_'.matchAsPrefix(type.toString()) != null;
}
/// Returns the exception most recently caught by the Flutter framework.
///
/// See [TestWidgetsFlutterBinding.takeException] for details.
......
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