Skip to content

Commit ff1dbcd

Browse files
authored
Add geometry getters to Flutter Driver (flutter#32302)
1 parent 8b11448 commit ff1dbcd

File tree

5 files changed

+301
-0
lines changed

5 files changed

+301
-0
lines changed
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
// Copyright 2019 The Chromium Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'package:flutter_driver/src/common/enum_util.dart';
6+
7+
import 'find.dart';
8+
import 'message.dart';
9+
10+
/// Offset types that can be requested by [GetOffset].
11+
enum OffsetType {
12+
/// The top left point.
13+
topLeft,
14+
15+
/// The top right point.
16+
topRight,
17+
18+
/// The bottom left point.
19+
bottomLeft,
20+
21+
/// The bottom right point.
22+
bottomRight,
23+
24+
/// The center point.
25+
center,
26+
}
27+
28+
EnumIndex<OffsetType> _offsetTypeIndex = EnumIndex<OffsetType>(OffsetType.values);
29+
30+
/// A Flutter Driver command that return the [offsetType] from the RenderObject
31+
/// identified by [finder].
32+
class GetOffset extends CommandWithTarget {
33+
/// The `finder` looks for an element to get its rect.
34+
GetOffset(SerializableFinder finder, this.offsetType, { Duration timeout }) : super(finder, timeout: timeout);
35+
36+
/// Deserializes this command from the value generated by [serialize].
37+
GetOffset.deserialize(Map<String, dynamic> json)
38+
: offsetType = _offsetTypeIndex.lookupBySimpleName(json['offsetType']),
39+
super.deserialize(json);
40+
41+
@override
42+
Map<String, String> serialize() => super.serialize()..addAll(<String, String>{
43+
'offsetType': _offsetTypeIndex.toSimpleName(offsetType),
44+
});
45+
46+
/// The type of the requested offset.
47+
final OffsetType offsetType;
48+
49+
@override
50+
final String kind = 'get_offset';
51+
}
52+
53+
/// The result of the [GetRect] command.
54+
class GetOffsetResult extends Result {
55+
/// Creates a result with the offset defined by [dx] and [dy].
56+
GetOffsetResult({ this.dx = 0.0, this.dy = 0.0});
57+
58+
/// The x component of the offset.
59+
final double dx;
60+
61+
/// The y component of the offset.
62+
final double dy;
63+
64+
/// Deserializes the result from JSON.
65+
static GetOffsetResult fromJson(Map<String, dynamic> json) {
66+
return GetOffsetResult(
67+
dx: json['dx'],
68+
dy: json['dy'],
69+
);
70+
}
71+
72+
@override
73+
Map<String, dynamic> toJson() => <String, double>{
74+
'dx': dx,
75+
'dy': dy,
76+
};
77+
}

packages/flutter_driver/lib/src/driver/driver.dart

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import '../common/error.dart';
1919
import '../common/find.dart';
2020
import '../common/frame_sync.dart';
2121
import '../common/fuchsia_compat.dart';
22+
import '../common/geometry.dart';
2223
import '../common/gesture.dart';
2324
import '../common/health.dart';
2425
import '../common/message.dart';
@@ -466,6 +467,37 @@ class FlutterDriver {
466467
await _sendCommand(WaitUntilNoTransientCallbacks(timeout: timeout));
467468
}
468469

470+
Future<DriverOffset> _getOffset(SerializableFinder finder, OffsetType type, { Duration timeout }) async {
471+
final GetOffset command = GetOffset(finder, type, timeout: timeout);
472+
final GetOffsetResult result = GetOffsetResult.fromJson(await _sendCommand(command));
473+
return DriverOffset(result.dx, result.dy);
474+
}
475+
476+
/// Returns the point at the top left of the widget identified by `finder`.
477+
Future<DriverOffset> getTopLeft(SerializableFinder finder, { Duration timeout }) async {
478+
return _getOffset(finder, OffsetType.topLeft, timeout: timeout);
479+
}
480+
481+
/// Returns the point at the top right of the widget identified by `finder`.
482+
Future<DriverOffset> getTopRight(SerializableFinder finder, { Duration timeout }) async {
483+
return _getOffset(finder, OffsetType.topRight, timeout: timeout);
484+
}
485+
486+
/// Returns the point at the bottom left of the widget identified by `finder`.
487+
Future<DriverOffset> getBottomLeft(SerializableFinder finder, { Duration timeout }) async {
488+
return _getOffset(finder, OffsetType.bottomLeft, timeout: timeout);
489+
}
490+
491+
/// Returns the point at the bottom right of the widget identified by `finder`.
492+
Future<DriverOffset> getBottomRight(SerializableFinder finder, { Duration timeout }) async {
493+
return _getOffset(finder, OffsetType.bottomRight, timeout: timeout);
494+
}
495+
496+
/// Returns the point at the center of the widget identified by `finder`.
497+
Future<DriverOffset> getCenter(SerializableFinder finder, { Duration timeout }) async {
498+
return _getOffset(finder, OffsetType.center, timeout: timeout);
499+
}
500+
469501
/// Tell the driver to perform a scrolling action.
470502
///
471503
/// A scrolling action begins with a "pointer down" event, which commonly maps
@@ -986,3 +1018,29 @@ class CommonFinders {
9861018
/// Finds the back button on a Material or Cupertino page's scaffold.
9871019
SerializableFinder pageBack() => PageBack();
9881020
}
1021+
1022+
/// An immutable 2D floating-point offset used by Flutter Driver.
1023+
class DriverOffset {
1024+
/// Creates an offset.
1025+
const DriverOffset(this.dx, this.dy);
1026+
1027+
/// The x component of the offset.
1028+
final double dx;
1029+
1030+
/// The y component of the offset.
1031+
final double dy;
1032+
1033+
@override
1034+
String toString() => '$runtimeType($dx, $dy)';
1035+
1036+
@override
1037+
bool operator ==(dynamic other) {
1038+
if (other is! DriverOffset)
1039+
return false;
1040+
final DriverOffset typedOther = other;
1041+
return dx == typedOther.dx && dy == typedOther.dy;
1042+
}
1043+
1044+
@override
1045+
int get hashCode => dx.hashCode + dy.hashCode;
1046+
}

packages/flutter_driver/lib/src/extension/extension.dart

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import 'package:flutter_test/flutter_test.dart';
2020
import '../common/error.dart';
2121
import '../common/find.dart';
2222
import '../common/frame_sync.dart';
23+
import '../common/geometry.dart';
2324
import '../common/gesture.dart';
2425
import '../common/health.dart';
2526
import '../common/message.dart';
@@ -112,6 +113,7 @@ class FlutterDriverExtension {
112113
'waitForAbsent': _waitForAbsent,
113114
'waitUntilNoTransientCallbacks': _waitUntilNoTransientCallbacks,
114115
'get_semantics_id': _getSemanticsId,
116+
'get_offset': _getOffset,
115117
});
116118

117119
_commandDeserializers.addAll(<String, CommandDeserializerCallback>{
@@ -130,6 +132,7 @@ class FlutterDriverExtension {
130132
'waitForAbsent': (Map<String, String> params) => WaitForAbsent.deserialize(params),
131133
'waitUntilNoTransientCallbacks': (Map<String, String> params) => WaitUntilNoTransientCallbacks.deserialize(params),
132134
'get_semantics_id': (Map<String, String> params) => GetSemanticsId.deserialize(params),
135+
'get_offset': (Map<String, String> params) => GetOffset.deserialize(params),
133136
});
134137

135138
_finders.addAll(<String, FinderConstructor>{
@@ -358,6 +361,33 @@ class FlutterDriverExtension {
358361
return GetSemanticsIdResult(node.id);
359362
}
360363

364+
Future<GetOffsetResult> _getOffset(Command command) async {
365+
final GetOffset getOffsetCommand = command;
366+
final Finder finder = await _waitForElement(_createFinder(getOffsetCommand.finder));
367+
final Element element = finder.evaluate().single;
368+
final RenderBox box = element.renderObject;
369+
Offset localPoint;
370+
switch (getOffsetCommand.offsetType) {
371+
case OffsetType.topLeft:
372+
localPoint = Offset.zero;
373+
break;
374+
case OffsetType.topRight:
375+
localPoint = box.size.topRight(Offset.zero);
376+
break;
377+
case OffsetType.bottomLeft:
378+
localPoint = box.size.bottomLeft(Offset.zero);
379+
break;
380+
case OffsetType.bottomRight:
381+
localPoint = box.size.bottomRight(Offset.zero);
382+
break;
383+
case OffsetType.center:
384+
localPoint = box.size.center(Offset.zero);
385+
break;
386+
}
387+
final Offset globalPoint = box.localToGlobal(localPoint);
388+
return GetOffsetResult(dx: globalPoint.dx, dy: globalPoint.dy);
389+
}
390+
361391
Future<ScrollResult> _scroll(Command command) async {
362392
final Scroll scrollCommand = command;
363393
final Finder target = await _waitForElement(_createFinder(scrollCommand.finder));

packages/flutter_driver/test/flutter_driver_test.dart

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,111 @@ void main() {
261261
});
262262
});
263263

264+
group('getOffset', () {
265+
test('requires a target reference', () async {
266+
expect(driver.getCenter(null), throwsA(isInstanceOf<DriverError>()));
267+
expect(driver.getTopLeft(null), throwsA(isInstanceOf<DriverError>()));
268+
expect(driver.getTopRight(null), throwsA(isInstanceOf<DriverError>()));
269+
expect(driver.getBottomLeft(null), throwsA(isInstanceOf<DriverError>()));
270+
expect(driver.getBottomRight(null), throwsA(isInstanceOf<DriverError>()));
271+
});
272+
273+
test('sends the getCenter command', () async {
274+
when(mockIsolate.invokeExtension(any, any)).thenAnswer((Invocation i) {
275+
expect(i.positionalArguments[1], <String, dynamic>{
276+
'command': 'get_offset',
277+
'offsetType': 'center',
278+
'timeout': _kSerializedTestTimeout,
279+
'finderType': 'ByValueKey',
280+
'keyValueString': '123',
281+
'keyValueType': 'int',
282+
});
283+
return makeMockResponse(<String, double>{
284+
'dx': 11,
285+
'dy': 12,
286+
});
287+
});
288+
final DriverOffset result = await driver.getCenter(find.byValueKey(123), timeout: _kTestTimeout);
289+
expect(result, const DriverOffset(11, 12));
290+
});
291+
292+
test('sends the getTopLeft command', () async {
293+
when(mockIsolate.invokeExtension(any, any)).thenAnswer((Invocation i) {
294+
expect(i.positionalArguments[1], <String, dynamic>{
295+
'command': 'get_offset',
296+
'offsetType': 'topLeft',
297+
'timeout': _kSerializedTestTimeout,
298+
'finderType': 'ByValueKey',
299+
'keyValueString': '123',
300+
'keyValueType': 'int',
301+
});
302+
return makeMockResponse(<String, double>{
303+
'dx': 11,
304+
'dy': 12,
305+
});
306+
});
307+
final DriverOffset result = await driver.getTopLeft(find.byValueKey(123), timeout: _kTestTimeout);
308+
expect(result, const DriverOffset(11, 12));
309+
});
310+
311+
test('sends the getTopRight command', () async {
312+
when(mockIsolate.invokeExtension(any, any)).thenAnswer((Invocation i) {
313+
expect(i.positionalArguments[1], <String, dynamic>{
314+
'command': 'get_offset',
315+
'offsetType': 'topRight',
316+
'timeout': _kSerializedTestTimeout,
317+
'finderType': 'ByValueKey',
318+
'keyValueString': '123',
319+
'keyValueType': 'int',
320+
});
321+
return makeMockResponse(<String, double>{
322+
'dx': 11,
323+
'dy': 12,
324+
});
325+
});
326+
final DriverOffset result = await driver.getTopRight(find.byValueKey(123), timeout: _kTestTimeout);
327+
expect(result, const DriverOffset(11, 12));
328+
});
329+
330+
test('sends the getBottomLeft command', () async {
331+
when(mockIsolate.invokeExtension(any, any)).thenAnswer((Invocation i) {
332+
expect(i.positionalArguments[1], <String, dynamic>{
333+
'command': 'get_offset',
334+
'offsetType': 'bottomLeft',
335+
'timeout': _kSerializedTestTimeout,
336+
'finderType': 'ByValueKey',
337+
'keyValueString': '123',
338+
'keyValueType': 'int',
339+
});
340+
return makeMockResponse(<String, double>{
341+
'dx': 11,
342+
'dy': 12,
343+
});
344+
});
345+
final DriverOffset result = await driver.getBottomLeft(find.byValueKey(123), timeout: _kTestTimeout);
346+
expect(result, const DriverOffset(11, 12));
347+
});
348+
349+
test('sends the getBottomRight command', () async {
350+
when(mockIsolate.invokeExtension(any, any)).thenAnswer((Invocation i) {
351+
expect(i.positionalArguments[1], <String, dynamic>{
352+
'command': 'get_offset',
353+
'offsetType': 'bottomRight',
354+
'timeout': _kSerializedTestTimeout,
355+
'finderType': 'ByValueKey',
356+
'keyValueString': '123',
357+
'keyValueType': 'int',
358+
});
359+
return makeMockResponse(<String, double>{
360+
'dx': 11,
361+
'dy': 12,
362+
});
363+
});
364+
final DriverOffset result = await driver.getBottomRight(find.byValueKey(123), timeout: _kTestTimeout);
365+
expect(result, const DriverOffset(11, 12));
366+
});
367+
});
368+
264369
group('clearTimeline', () {
265370
test('clears timeline', () async {
266371
bool clearWasCalled = false;

packages/flutter_driver/test/src/extension_test.dart

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import 'package:flutter/rendering.dart';
66
import 'package:flutter/scheduler.dart';
77
import 'package:flutter/widgets.dart';
88
import 'package:flutter_driver/src/common/find.dart';
9+
import 'package:flutter_driver/src/common/geometry.dart';
910
import 'package:flutter_driver/src/common/request_data.dart';
1011
import 'package:flutter_driver/src/extension/extension.dart';
1112
import 'package:flutter_test/flutter_test.dart';
@@ -120,4 +121,34 @@ void main() {
120121
semantics.dispose();
121122
});
122123
});
124+
125+
testWidgets('getOffset', (WidgetTester tester) async {
126+
final FlutterDriverExtension extension = FlutterDriverExtension((String arg) async => '', true);
127+
128+
Future<Offset> getOffset(OffsetType offset) async {
129+
final Map<String, Object> arguments = GetOffset(ByValueKey(1), offset).serialize();
130+
final GetOffsetResult result = GetOffsetResult.fromJson((await extension.call(arguments))['response']);
131+
return Offset(result.dx, result.dy);
132+
}
133+
134+
await tester.pumpWidget(
135+
Align(
136+
alignment: Alignment.topLeft,
137+
child: Transform.translate(
138+
offset: const Offset(40, 30),
139+
child: Container(
140+
key: const ValueKey<int>(1),
141+
width: 100,
142+
height: 120,
143+
),
144+
),
145+
),
146+
);
147+
148+
expect(await getOffset(OffsetType.topLeft), const Offset(40, 30));
149+
expect(await getOffset(OffsetType.topRight), const Offset(40 + 100.0, 30));
150+
expect(await getOffset(OffsetType.bottomLeft), const Offset(40, 30 + 120.0));
151+
expect(await getOffset(OffsetType.bottomRight), const Offset(40 + 100.0, 30 + 120.0));
152+
expect(await getOffset(OffsetType.center), const Offset(40 + (100 / 2), 30 + (120 / 2)));
153+
});
123154
}

0 commit comments

Comments
 (0)