Unverified Commit ff1dbcde authored by Michael Goderbauer's avatar Michael Goderbauer Committed by GitHub

Add geometry getters to Flutter Driver (#32302)

parent 8b114482
// 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<OffsetType> _offsetTypeIndex = EnumIndex<OffsetType>(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<String, dynamic> json)
: offsetType = _offsetTypeIndex.lookupBySimpleName(json['offsetType']),
super.deserialize(json);
@override
Map<String, String> serialize() => super.serialize()..addAll(<String, String>{
'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<String, dynamic> json) {
return GetOffsetResult(
dx: json['dx'],
dy: json['dy'],
);
}
@override
Map<String, dynamic> toJson() => <String, double>{
'dx': dx,
'dy': dy,
};
}
...@@ -19,6 +19,7 @@ import '../common/error.dart'; ...@@ -19,6 +19,7 @@ import '../common/error.dart';
import '../common/find.dart'; import '../common/find.dart';
import '../common/frame_sync.dart'; import '../common/frame_sync.dart';
import '../common/fuchsia_compat.dart'; import '../common/fuchsia_compat.dart';
import '../common/geometry.dart';
import '../common/gesture.dart'; import '../common/gesture.dart';
import '../common/health.dart'; import '../common/health.dart';
import '../common/message.dart'; import '../common/message.dart';
...@@ -466,6 +467,37 @@ class FlutterDriver { ...@@ -466,6 +467,37 @@ class FlutterDriver {
await _sendCommand(WaitUntilNoTransientCallbacks(timeout: timeout)); await _sendCommand(WaitUntilNoTransientCallbacks(timeout: timeout));
} }
Future<DriverOffset> _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<DriverOffset> 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<DriverOffset> 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<DriverOffset> 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<DriverOffset> 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<DriverOffset> getCenter(SerializableFinder finder, { Duration timeout }) async {
return _getOffset(finder, OffsetType.center, timeout: timeout);
}
/// Tell the driver to perform a scrolling action. /// Tell the driver to perform a scrolling action.
/// ///
/// A scrolling action begins with a "pointer down" event, which commonly maps /// A scrolling action begins with a "pointer down" event, which commonly maps
...@@ -986,3 +1018,29 @@ class CommonFinders { ...@@ -986,3 +1018,29 @@ class CommonFinders {
/// Finds the back button on a Material or Cupertino page's scaffold. /// Finds the back button on a Material or Cupertino page's scaffold.
SerializableFinder pageBack() => PageBack(); 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;
}
...@@ -20,6 +20,7 @@ import 'package:flutter_test/flutter_test.dart'; ...@@ -20,6 +20,7 @@ import 'package:flutter_test/flutter_test.dart';
import '../common/error.dart'; import '../common/error.dart';
import '../common/find.dart'; import '../common/find.dart';
import '../common/frame_sync.dart'; import '../common/frame_sync.dart';
import '../common/geometry.dart';
import '../common/gesture.dart'; import '../common/gesture.dart';
import '../common/health.dart'; import '../common/health.dart';
import '../common/message.dart'; import '../common/message.dart';
...@@ -112,6 +113,7 @@ class FlutterDriverExtension { ...@@ -112,6 +113,7 @@ class FlutterDriverExtension {
'waitForAbsent': _waitForAbsent, 'waitForAbsent': _waitForAbsent,
'waitUntilNoTransientCallbacks': _waitUntilNoTransientCallbacks, 'waitUntilNoTransientCallbacks': _waitUntilNoTransientCallbacks,
'get_semantics_id': _getSemanticsId, 'get_semantics_id': _getSemanticsId,
'get_offset': _getOffset,
}); });
_commandDeserializers.addAll(<String, CommandDeserializerCallback>{ _commandDeserializers.addAll(<String, CommandDeserializerCallback>{
...@@ -130,6 +132,7 @@ class FlutterDriverExtension { ...@@ -130,6 +132,7 @@ class FlutterDriverExtension {
'waitForAbsent': (Map<String, String> params) => WaitForAbsent.deserialize(params), 'waitForAbsent': (Map<String, String> params) => WaitForAbsent.deserialize(params),
'waitUntilNoTransientCallbacks': (Map<String, String> params) => WaitUntilNoTransientCallbacks.deserialize(params), 'waitUntilNoTransientCallbacks': (Map<String, String> params) => WaitUntilNoTransientCallbacks.deserialize(params),
'get_semantics_id': (Map<String, String> params) => GetSemanticsId.deserialize(params), 'get_semantics_id': (Map<String, String> params) => GetSemanticsId.deserialize(params),
'get_offset': (Map<String, String> params) => GetOffset.deserialize(params),
}); });
_finders.addAll(<String, FinderConstructor>{ _finders.addAll(<String, FinderConstructor>{
...@@ -358,6 +361,33 @@ class FlutterDriverExtension { ...@@ -358,6 +361,33 @@ class FlutterDriverExtension {
return GetSemanticsIdResult(node.id); return GetSemanticsIdResult(node.id);
} }
Future<GetOffsetResult> _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<ScrollResult> _scroll(Command command) async { Future<ScrollResult> _scroll(Command command) async {
final Scroll scrollCommand = command; final Scroll scrollCommand = command;
final Finder target = await _waitForElement(_createFinder(scrollCommand.finder)); final Finder target = await _waitForElement(_createFinder(scrollCommand.finder));
......
...@@ -261,6 +261,111 @@ void main() { ...@@ -261,6 +261,111 @@ void main() {
}); });
}); });
group('getOffset', () {
test('requires a target reference', () async {
expect(driver.getCenter(null), throwsA(isInstanceOf<DriverError>()));
expect(driver.getTopLeft(null), throwsA(isInstanceOf<DriverError>()));
expect(driver.getTopRight(null), throwsA(isInstanceOf<DriverError>()));
expect(driver.getBottomLeft(null), throwsA(isInstanceOf<DriverError>()));
expect(driver.getBottomRight(null), throwsA(isInstanceOf<DriverError>()));
});
test('sends the getCenter command', () async {
when(mockIsolate.invokeExtension(any, any)).thenAnswer((Invocation i) {
expect(i.positionalArguments[1], <String, dynamic>{
'command': 'get_offset',
'offsetType': 'center',
'timeout': _kSerializedTestTimeout,
'finderType': 'ByValueKey',
'keyValueString': '123',
'keyValueType': 'int',
});
return makeMockResponse(<String, double>{
'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], <String, dynamic>{
'command': 'get_offset',
'offsetType': 'topLeft',
'timeout': _kSerializedTestTimeout,
'finderType': 'ByValueKey',
'keyValueString': '123',
'keyValueType': 'int',
});
return makeMockResponse(<String, double>{
'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], <String, dynamic>{
'command': 'get_offset',
'offsetType': 'topRight',
'timeout': _kSerializedTestTimeout,
'finderType': 'ByValueKey',
'keyValueString': '123',
'keyValueType': 'int',
});
return makeMockResponse(<String, double>{
'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], <String, dynamic>{
'command': 'get_offset',
'offsetType': 'bottomLeft',
'timeout': _kSerializedTestTimeout,
'finderType': 'ByValueKey',
'keyValueString': '123',
'keyValueType': 'int',
});
return makeMockResponse(<String, double>{
'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], <String, dynamic>{
'command': 'get_offset',
'offsetType': 'bottomRight',
'timeout': _kSerializedTestTimeout,
'finderType': 'ByValueKey',
'keyValueString': '123',
'keyValueType': 'int',
});
return makeMockResponse(<String, double>{
'dx': 11,
'dy': 12,
});
});
final DriverOffset result = await driver.getBottomRight(find.byValueKey(123), timeout: _kTestTimeout);
expect(result, const DriverOffset(11, 12));
});
});
group('clearTimeline', () { group('clearTimeline', () {
test('clears timeline', () async { test('clears timeline', () async {
bool clearWasCalled = false; bool clearWasCalled = false;
......
...@@ -6,6 +6,7 @@ import 'package:flutter/rendering.dart'; ...@@ -6,6 +6,7 @@ import 'package:flutter/rendering.dart';
import 'package:flutter/scheduler.dart'; import 'package:flutter/scheduler.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter_driver/src/common/find.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/common/request_data.dart';
import 'package:flutter_driver/src/extension/extension.dart'; import 'package:flutter_driver/src/extension/extension.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
...@@ -120,4 +121,34 @@ void main() { ...@@ -120,4 +121,34 @@ void main() {
semantics.dispose(); semantics.dispose();
}); });
}); });
testWidgets('getOffset', (WidgetTester tester) async {
final FlutterDriverExtension extension = FlutterDriverExtension((String arg) async => '', true);
Future<Offset> getOffset(OffsetType offset) async {
final Map<String, Object> 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<int>(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)));
});
} }
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