// Copyright 2015 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/gestures.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'test_pointer.dart'; typedef Point SizeToPointFunction(Size size); /// Helper class for flutter tests providing event dispatch. /// /// This class provides hooks for accessing the rendering tree and dispatching /// fake tap/drag/etc. events. class Instrumentation { Instrumentation({ Widgeteer binding }) : this.binding = binding ?? WidgetFlutterBinding.ensureInitialized(); final Widgeteer binding; /// Returns a list of all the [Layer] objects in the rendering. List get layers => _layers(binding.renderView.layer); // TODO(ianh): This should not be O(N) hidden behind a getter! List _layers(Layer layer) { List result = [layer]; if (layer is ContainerLayer) { ContainerLayer root = layer; Layer child = root.firstChild; while (child != null) { result.addAll(_layers(child)); child = child.nextSibling; } } return result; } /// Walks all the elements in the tree, in depth-first pre-order, /// calling the given function for each one. void walkElements(ElementVisitor visitor) { void walk(Element element) { visitor(element); element.visitChildren(walk); } binding.renderViewElement.visitChildren(walk); } /// Returns the first element that for which the given predicate /// function returns true, if any, or null if the predicate function /// never returns true. Element findElement(bool predicate(Element element)) { try { walkElements((Element element) { if (predicate(element)) throw element; }); } on Element catch (e) { return e; } return null; } /// Returns the first element that corresponds to a widget with the /// given [Key], or null if there is no such element. Element findElementByKey(Key key) { return findElement((Element element) => element.widget.key == key); } /// Returns the first element that corresponds to a [Text] widget /// whose data is the given string, or null if there is no such /// element. Element findText(String text) { return findElement((Element element) { if (element.widget is! Text) return false; Text textWidget = element.widget; return textWidget.data == text; }); } /// Returns the first [Widget] of the given [runtimeType], if any. Returns /// null if there is no matching widget. Widget findWidgetOfType(Type type) { Element element = findElement((Element element) { return element.widget.runtimeType == type; }); return element?.widget; } /// Returns the [State] object of the first element whose state has /// the given [runtimeType], if any. Returns null if there is no /// matching element. State findStateOfType(Type type) { StatefulElement element = findElement((Element element) { return element is StatefulElement && element.state.runtimeType == type; }); return element?.state; } /// Returns the [State] object of the first element whose /// configuration is the given widget, if any. Returns null if the /// given configuration is not that of a stateful widget or if there /// is no matching element. State findStateByConfig(Widget config) { StatefulElement element = findElement((Element element) { return element is StatefulElement && element.state.config == config; }); return element?.state; } /// Returns the point at the center of the given element. Point getCenter(Element element) { return _getElementPoint(element, (Size size) => size.center(Point.origin)); } /// Returns the point at the top left of the given element. Point getTopLeft(Element element) { return _getElementPoint(element, (_) => Point.origin); } /// Returns the point at the top right of the given element. This /// point is not inside the object's hit test area. Point getTopRight(Element element) { return _getElementPoint(element, (Size size) => size.topRight(Point.origin)); } /// Returns the point at the bottom left of the given element. This /// point is not inside the object's hit test area. Point getBottomLeft(Element element) { return _getElementPoint(element, (Size size) => size.bottomLeft(Point.origin)); } /// Returns the point at the bottom right of the given element. This /// point is not inside the object's hit test area. Point getBottomRight(Element element) { return _getElementPoint(element, (Size size) => size.bottomRight(Point.origin)); } /// Returns the size of the given element. This is only valid once /// the element's render object has been laid out at least once. Size getSize(Element element) { assert(element != null); RenderBox box = element.renderObject; assert(box != null); return box.size; } Point _getElementPoint(Element element, SizeToPointFunction sizeToPoint) { assert(element != null); RenderBox box = element.renderObject; assert(box != null); return box.localToGlobal(sizeToPoint(box.size)); } /// Dispatch a pointer down / pointer up sequence at the center of /// the given element, assuming it is exposed. If the center of the /// element is not exposed, this might send events to another /// object. void tap(Element element, { int pointer: 1 }) { tapAt(getCenter(element), pointer: pointer); } /// Dispatch a pointer down / pointer up sequence at the given /// location. void tapAt(Point location, { int pointer: 1 }) { HitTestResult result = _hitTest(location); TestPointer p = new TestPointer(pointer); binding.dispatchEvent(p.down(location), result); binding.dispatchEvent(p.up(), result); } /// Attempts a fling gesture starting from the center of the given /// element, moving the given distance, reaching the given velocity. /// /// If the middle of the element is not exposed, this might send /// events to another object. void fling(Element element, Offset offset, double velocity, { int pointer: 1 }) { flingFrom(getCenter(element), offset, velocity, pointer: pointer); } /// Attempts a fling gesture starting from the given location, /// moving the given distance, reaching the given velocity. void flingFrom(Point startLocation, Offset offset, double velocity, { int pointer: 1 }) { assert(offset.distance > 0.0); assert(velocity != 0.0); // velocity is pixels/second final TestPointer p = new TestPointer(pointer); final HitTestResult result = _hitTest(startLocation); const int kMoveCount = 50; // Needs to be >= kHistorySize, see _LeastSquaresVelocityTrackerStrategy final double timeStampDelta = 1000.0 * offset.distance / (kMoveCount * velocity); double timeStamp = 0.0; binding.dispatchEvent(p.down(startLocation, timeStamp: new Duration(milliseconds: timeStamp.round())), result); for(int i = 0; i <= kMoveCount; i++) { final Point location = startLocation + Offset.lerp(Offset.zero, offset, i / kMoveCount); binding.dispatchEvent(p.move(location, timeStamp: new Duration(milliseconds: timeStamp.round())), result); timeStamp += timeStampDelta; } binding.dispatchEvent(p.up(timeStamp: new Duration(milliseconds: timeStamp.round())), result); } /// Attempts to drag the given element by the given offset, by /// starting a drag in the middle of the element. /// /// If the middle of the element is not exposed, this might send /// events to another object. void scroll(Element element, Offset offset, { int pointer: 1 }) { scrollAt(getCenter(element), offset, pointer: pointer); } /// Attempts a drag gesture consisting of a pointer down, a move by /// the given offset, and a pointer up. void scrollAt(Point startLocation, Offset offset, { int pointer: 1 }) { Point endLocation = startLocation + offset; TestPointer p = new TestPointer(pointer); // Events for the entire press-drag-release gesture are dispatched // to the widgets "hit" by the pointer down event. HitTestResult result = _hitTest(startLocation); binding.dispatchEvent(p.down(startLocation), result); binding.dispatchEvent(p.move(endLocation), result); binding.dispatchEvent(p.up(), result); } /// Begins a gesture at a particular point, and returns the /// [TestGesture] object which you can use to continue the gesture. TestGesture startGesture(Point downLocation, { int pointer: 1 }) { return new TestGesture(downLocation, pointer: pointer); } HitTestResult _hitTest(Point location) { HitTestResult result = new HitTestResult(); binding.hitTest(result, location); return result; } }