From ff1dbcdeb6260d0db048e417ff31de408fb51487 Mon Sep 17 00:00:00 2001 From: Michael Goderbauer Date: Thu, 9 May 2019 09:50:02 +0200 Subject: [PATCH] Add geometry getters to Flutter Driver (#32302) --- .../lib/src/common/geometry.dart | 77 +++++++++++++ .../flutter_driver/lib/src/driver/driver.dart | 58 ++++++++++ .../lib/src/extension/extension.dart | 30 +++++ .../test/flutter_driver_test.dart | 105 ++++++++++++++++++ .../test/src/extension_test.dart | 31 ++++++ 5 files changed, 301 insertions(+) create mode 100644 packages/flutter_driver/lib/src/common/geometry.dart diff --git a/packages/flutter_driver/lib/src/common/geometry.dart b/packages/flutter_driver/lib/src/common/geometry.dart new file mode 100644 index 0000000000..cfedb9e465 --- /dev/null +++ b/packages/flutter_driver/lib/src/common/geometry.dart @@ -0,0 +1,77 @@ +// Copyright 2019 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_driver/src/common/enum_util.dart'; + +import 'find.dart'; +import 'message.dart'; + +/// Offset types that can be requested by [GetOffset]. +enum OffsetType { + /// The top left point. + topLeft, + + /// The top right point. + topRight, + + /// The bottom left point. + bottomLeft, + + /// The bottom right point. + bottomRight, + + /// The center point. + center, +} + +EnumIndex _offsetTypeIndex = EnumIndex(OffsetType.values); + +/// A Flutter Driver command that return the [offsetType] from the RenderObject +/// identified by [finder]. +class GetOffset extends CommandWithTarget { + /// The `finder` looks for an element to get its rect. + GetOffset(SerializableFinder finder, this.offsetType, { Duration timeout }) : super(finder, timeout: timeout); + + /// Deserializes this command from the value generated by [serialize]. + GetOffset.deserialize(Map json) + : offsetType = _offsetTypeIndex.lookupBySimpleName(json['offsetType']), + super.deserialize(json); + + @override + Map serialize() => super.serialize()..addAll({ + 'offsetType': _offsetTypeIndex.toSimpleName(offsetType), + }); + + /// The type of the requested offset. + final OffsetType offsetType; + + @override + final String kind = 'get_offset'; +} + +/// The result of the [GetRect] command. +class GetOffsetResult extends Result { + /// Creates a result with the offset defined by [dx] and [dy]. + GetOffsetResult({ this.dx = 0.0, this.dy = 0.0}); + + /// The x component of the offset. + final double dx; + + /// The y component of the offset. + final double dy; + + /// Deserializes the result from JSON. + static GetOffsetResult fromJson(Map json) { + return GetOffsetResult( + dx: json['dx'], + dy: json['dy'], + ); + } + + @override + Map toJson() => { + 'dx': dx, + 'dy': dy, + }; +} diff --git a/packages/flutter_driver/lib/src/driver/driver.dart b/packages/flutter_driver/lib/src/driver/driver.dart index bf58352d0d..7071c4ac73 100644 --- a/packages/flutter_driver/lib/src/driver/driver.dart +++ b/packages/flutter_driver/lib/src/driver/driver.dart @@ -19,6 +19,7 @@ import '../common/error.dart'; import '../common/find.dart'; import '../common/frame_sync.dart'; import '../common/fuchsia_compat.dart'; +import '../common/geometry.dart'; import '../common/gesture.dart'; import '../common/health.dart'; import '../common/message.dart'; @@ -466,6 +467,37 @@ class FlutterDriver { await _sendCommand(WaitUntilNoTransientCallbacks(timeout: timeout)); } + Future _getOffset(SerializableFinder finder, OffsetType type, { Duration timeout }) async { + final GetOffset command = GetOffset(finder, type, timeout: timeout); + final GetOffsetResult result = GetOffsetResult.fromJson(await _sendCommand(command)); + return DriverOffset(result.dx, result.dy); + } + + /// Returns the point at the top left of the widget identified by `finder`. + Future getTopLeft(SerializableFinder finder, { Duration timeout }) async { + return _getOffset(finder, OffsetType.topLeft, timeout: timeout); + } + + /// Returns the point at the top right of the widget identified by `finder`. + Future getTopRight(SerializableFinder finder, { Duration timeout }) async { + return _getOffset(finder, OffsetType.topRight, timeout: timeout); + } + + /// Returns the point at the bottom left of the widget identified by `finder`. + Future getBottomLeft(SerializableFinder finder, { Duration timeout }) async { + return _getOffset(finder, OffsetType.bottomLeft, timeout: timeout); + } + + /// Returns the point at the bottom right of the widget identified by `finder`. + Future getBottomRight(SerializableFinder finder, { Duration timeout }) async { + return _getOffset(finder, OffsetType.bottomRight, timeout: timeout); + } + + /// Returns the point at the center of the widget identified by `finder`. + Future getCenter(SerializableFinder finder, { Duration timeout }) async { + return _getOffset(finder, OffsetType.center, timeout: timeout); + } + /// Tell the driver to perform a scrolling action. /// /// A scrolling action begins with a "pointer down" event, which commonly maps @@ -986,3 +1018,29 @@ class CommonFinders { /// Finds the back button on a Material or Cupertino page's scaffold. SerializableFinder pageBack() => PageBack(); } + +/// An immutable 2D floating-point offset used by Flutter Driver. +class DriverOffset { + /// Creates an offset. + const DriverOffset(this.dx, this.dy); + + /// The x component of the offset. + final double dx; + + /// The y component of the offset. + final double dy; + + @override + String toString() => '$runtimeType($dx, $dy)'; + + @override + bool operator ==(dynamic other) { + if (other is! DriverOffset) + return false; + final DriverOffset typedOther = other; + return dx == typedOther.dx && dy == typedOther.dy; + } + + @override + int get hashCode => dx.hashCode + dy.hashCode; +} diff --git a/packages/flutter_driver/lib/src/extension/extension.dart b/packages/flutter_driver/lib/src/extension/extension.dart index 4af79254dc..2c9c62e4b7 100644 --- a/packages/flutter_driver/lib/src/extension/extension.dart +++ b/packages/flutter_driver/lib/src/extension/extension.dart @@ -20,6 +20,7 @@ import 'package:flutter_test/flutter_test.dart'; import '../common/error.dart'; import '../common/find.dart'; import '../common/frame_sync.dart'; +import '../common/geometry.dart'; import '../common/gesture.dart'; import '../common/health.dart'; import '../common/message.dart'; @@ -112,6 +113,7 @@ class FlutterDriverExtension { 'waitForAbsent': _waitForAbsent, 'waitUntilNoTransientCallbacks': _waitUntilNoTransientCallbacks, 'get_semantics_id': _getSemanticsId, + 'get_offset': _getOffset, }); _commandDeserializers.addAll({ @@ -130,6 +132,7 @@ class FlutterDriverExtension { 'waitForAbsent': (Map params) => WaitForAbsent.deserialize(params), 'waitUntilNoTransientCallbacks': (Map params) => WaitUntilNoTransientCallbacks.deserialize(params), 'get_semantics_id': (Map params) => GetSemanticsId.deserialize(params), + 'get_offset': (Map params) => GetOffset.deserialize(params), }); _finders.addAll({ @@ -358,6 +361,33 @@ class FlutterDriverExtension { return GetSemanticsIdResult(node.id); } + Future _getOffset(Command command) async { + final GetOffset getOffsetCommand = command; + final Finder finder = await _waitForElement(_createFinder(getOffsetCommand.finder)); + final Element element = finder.evaluate().single; + final RenderBox box = element.renderObject; + Offset localPoint; + switch (getOffsetCommand.offsetType) { + case OffsetType.topLeft: + localPoint = Offset.zero; + break; + case OffsetType.topRight: + localPoint = box.size.topRight(Offset.zero); + break; + case OffsetType.bottomLeft: + localPoint = box.size.bottomLeft(Offset.zero); + break; + case OffsetType.bottomRight: + localPoint = box.size.bottomRight(Offset.zero); + break; + case OffsetType.center: + localPoint = box.size.center(Offset.zero); + break; + } + final Offset globalPoint = box.localToGlobal(localPoint); + return GetOffsetResult(dx: globalPoint.dx, dy: globalPoint.dy); + } + Future _scroll(Command command) async { final Scroll scrollCommand = command; final Finder target = await _waitForElement(_createFinder(scrollCommand.finder)); diff --git a/packages/flutter_driver/test/flutter_driver_test.dart b/packages/flutter_driver/test/flutter_driver_test.dart index c143354f3d..81b4c0930a 100644 --- a/packages/flutter_driver/test/flutter_driver_test.dart +++ b/packages/flutter_driver/test/flutter_driver_test.dart @@ -261,6 +261,111 @@ void main() { }); }); + group('getOffset', () { + test('requires a target reference', () async { + expect(driver.getCenter(null), throwsA(isInstanceOf())); + expect(driver.getTopLeft(null), throwsA(isInstanceOf())); + expect(driver.getTopRight(null), throwsA(isInstanceOf())); + expect(driver.getBottomLeft(null), throwsA(isInstanceOf())); + expect(driver.getBottomRight(null), throwsA(isInstanceOf())); + }); + + test('sends the getCenter command', () async { + when(mockIsolate.invokeExtension(any, any)).thenAnswer((Invocation i) { + expect(i.positionalArguments[1], { + 'command': 'get_offset', + 'offsetType': 'center', + 'timeout': _kSerializedTestTimeout, + 'finderType': 'ByValueKey', + 'keyValueString': '123', + 'keyValueType': 'int', + }); + return makeMockResponse({ + 'dx': 11, + 'dy': 12, + }); + }); + final DriverOffset result = await driver.getCenter(find.byValueKey(123), timeout: _kTestTimeout); + expect(result, const DriverOffset(11, 12)); + }); + + test('sends the getTopLeft command', () async { + when(mockIsolate.invokeExtension(any, any)).thenAnswer((Invocation i) { + expect(i.positionalArguments[1], { + 'command': 'get_offset', + 'offsetType': 'topLeft', + 'timeout': _kSerializedTestTimeout, + 'finderType': 'ByValueKey', + 'keyValueString': '123', + 'keyValueType': 'int', + }); + return makeMockResponse({ + 'dx': 11, + 'dy': 12, + }); + }); + final DriverOffset result = await driver.getTopLeft(find.byValueKey(123), timeout: _kTestTimeout); + expect(result, const DriverOffset(11, 12)); + }); + + test('sends the getTopRight command', () async { + when(mockIsolate.invokeExtension(any, any)).thenAnswer((Invocation i) { + expect(i.positionalArguments[1], { + 'command': 'get_offset', + 'offsetType': 'topRight', + 'timeout': _kSerializedTestTimeout, + 'finderType': 'ByValueKey', + 'keyValueString': '123', + 'keyValueType': 'int', + }); + return makeMockResponse({ + 'dx': 11, + 'dy': 12, + }); + }); + final DriverOffset result = await driver.getTopRight(find.byValueKey(123), timeout: _kTestTimeout); + expect(result, const DriverOffset(11, 12)); + }); + + test('sends the getBottomLeft command', () async { + when(mockIsolate.invokeExtension(any, any)).thenAnswer((Invocation i) { + expect(i.positionalArguments[1], { + 'command': 'get_offset', + 'offsetType': 'bottomLeft', + 'timeout': _kSerializedTestTimeout, + 'finderType': 'ByValueKey', + 'keyValueString': '123', + 'keyValueType': 'int', + }); + return makeMockResponse({ + 'dx': 11, + 'dy': 12, + }); + }); + final DriverOffset result = await driver.getBottomLeft(find.byValueKey(123), timeout: _kTestTimeout); + expect(result, const DriverOffset(11, 12)); + }); + + test('sends the getBottomRight command', () async { + when(mockIsolate.invokeExtension(any, any)).thenAnswer((Invocation i) { + expect(i.positionalArguments[1], { + 'command': 'get_offset', + 'offsetType': 'bottomRight', + 'timeout': _kSerializedTestTimeout, + 'finderType': 'ByValueKey', + 'keyValueString': '123', + 'keyValueType': 'int', + }); + return makeMockResponse({ + 'dx': 11, + 'dy': 12, + }); + }); + final DriverOffset result = await driver.getBottomRight(find.byValueKey(123), timeout: _kTestTimeout); + expect(result, const DriverOffset(11, 12)); + }); + }); + group('clearTimeline', () { test('clears timeline', () async { bool clearWasCalled = false; diff --git a/packages/flutter_driver/test/src/extension_test.dart b/packages/flutter_driver/test/src/extension_test.dart index 1d3718b36f..519100dd7c 100644 --- a/packages/flutter_driver/test/src/extension_test.dart +++ b/packages/flutter_driver/test/src/extension_test.dart @@ -6,6 +6,7 @@ import 'package:flutter/rendering.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_driver/src/common/find.dart'; +import 'package:flutter_driver/src/common/geometry.dart'; import 'package:flutter_driver/src/common/request_data.dart'; import 'package:flutter_driver/src/extension/extension.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -120,4 +121,34 @@ void main() { semantics.dispose(); }); }); + + testWidgets('getOffset', (WidgetTester tester) async { + final FlutterDriverExtension extension = FlutterDriverExtension((String arg) async => '', true); + + Future getOffset(OffsetType offset) async { + final Map arguments = GetOffset(ByValueKey(1), offset).serialize(); + final GetOffsetResult result = GetOffsetResult.fromJson((await extension.call(arguments))['response']); + return Offset(result.dx, result.dy); + } + + await tester.pumpWidget( + Align( + alignment: Alignment.topLeft, + child: Transform.translate( + offset: const Offset(40, 30), + child: Container( + key: const ValueKey(1), + width: 100, + height: 120, + ), + ), + ), + ); + + expect(await getOffset(OffsetType.topLeft), const Offset(40, 30)); + expect(await getOffset(OffsetType.topRight), const Offset(40 + 100.0, 30)); + expect(await getOffset(OffsetType.bottomLeft), const Offset(40, 30 + 120.0)); + expect(await getOffset(OffsetType.bottomRight), const Offset(40 + 100.0, 30 + 120.0)); + expect(await getOffset(OffsetType.center), const Offset(40 + (100 / 2), 30 + (120 / 2))); + }); }