Skip to content

Commit

Permalink
Make LiveTestWidgetsFlutterBinding work with setSurfaceSize and live …
Browse files Browse the repository at this point in the history
…tests (#86449)

This PR fixes several bugs related to live tests, adds more tests, and completes the documentation of several methods related to pointer events.
  • Loading branch information
dkwingsmt committed Jul 19, 2021
1 parent d2b1567 commit cd78190
Show file tree
Hide file tree
Showing 4 changed files with 207 additions and 56 deletions.
63 changes: 58 additions & 5 deletions packages/flutter/test/animation/live_binding_test.dart
Expand Up @@ -25,9 +25,12 @@ void main() {
border: Border.all(color: const Color.fromARGB(255, 0, 0, 0)),
),
child: Center(
child: GestureDetector(
onTap: () {},
child: const Text('Test'),
child: Material(
child: InkWell(
splashColor: Colors.blue,
child: const SizedBox(width: 40, height: 40),
onTap: () {},
),
),
),
),
Expand All @@ -40,12 +43,12 @@ void main() {
await tester.pumpFrames(target, const Duration(milliseconds: 50));

final TestGesture gesture1 = await tester.createGesture();
await gesture1.down(tester.getCenter(find.byType(Text)) + const Offset(10, 10));
await gesture1.down(tester.getCenter(find.byType(InkWell)) + const Offset(10, 10));

await tester.pumpFrames(target, const Duration(milliseconds: 100));

final TestGesture gesture2 = await tester.createGesture();
await gesture2.down(tester.getTopLeft(find.byType(Text)) + const Offset(30, -10));
await gesture2.down(tester.getTopLeft(find.byType(InkWell)) + const Offset(30, -10));
await gesture1.moveBy(const Offset(50, 50));

await tester.pumpFrames(target, const Duration(milliseconds: 100));
Expand All @@ -58,4 +61,54 @@ void main() {
matchesGoldenFile('LiveBinding.press.animation.png'),
);
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/42767

testWidgets('Should show event indicator for pointer events with setSurfaceSize', (WidgetTester tester) async {
final AnimationSheetBuilder animationSheet = AnimationSheetBuilder(frameSize: const Size(200, 200), allLayers: true);
final Widget target = Container(
padding: const EdgeInsets.fromLTRB(20, 10, 25, 20),
child: animationSheet.record(
MaterialApp(
home: Container(
decoration: BoxDecoration(
color: const Color.fromARGB(255, 128, 128, 128),
border: Border.all(color: const Color.fromARGB(255, 0, 0, 0)),
),
child: Center(
child: Material(
child: InkWell(
splashColor: Colors.blue,
child: const SizedBox(width: 40, height: 40),
onTap: () {},
),
),
),
),
),
),
);

await tester.binding.setSurfaceSize(const Size(300, 300));
await tester.pumpWidget(target);

await tester.pumpFrames(target, const Duration(milliseconds: 50));

final TestGesture gesture1 = await tester.createGesture();
await gesture1.down(tester.getCenter(find.byType(InkWell)) + const Offset(10, 10));

await tester.pumpFrames(target, const Duration(milliseconds: 100));

final TestGesture gesture2 = await tester.createGesture();
await gesture2.down(tester.getTopLeft(find.byType(InkWell)) + const Offset(30, -10));
await gesture1.moveBy(const Offset(50, 50));

await tester.pumpFrames(target, const Duration(milliseconds: 100));
await gesture1.up();
await gesture2.up();
await tester.pumpFrames(target, const Duration(milliseconds: 50));

await expectLater(
animationSheet.collate(6),
matchesGoldenFile('LiveBinding.press.animation.2.png'),
);
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/42767
}
81 changes: 58 additions & 23 deletions packages/flutter_test/lib/src/binding.dart
Expand Up @@ -119,6 +119,20 @@ mixin TestDefaultBinaryMessengerBinding on BindingBase, ServicesBinding {
/// that actually needs to make a network call should provide its own
/// `HttpClient` to the code making the call, so that it can appropriately mock
/// or fake responses.
///
/// ### Coordinate spaces
///
/// [TestWidgetsFlutterBinding] might be run on devices of different screen
/// sizes, while the testing widget is still told the same size to ensure
/// consistent results. Consequently, code that deals with positions (such as
/// pointer events or painting) must distinguish between two coordinate spaces:
///
/// * The _local coordinate space_ is the one used by the testing widget
/// (typically an 800 by 600 window, but can be altered by [setSurfaceSize]).
/// * The _global coordinate space_ is the one used by the device.
///
/// Positions can be transformed between coordinate spaces with [localToGlobal]
/// and [globalToLocal].
abstract class TestWidgetsFlutterBinding extends BindingBase
with SchedulerBinding,
ServicesBinding,
Expand Down Expand Up @@ -447,30 +461,51 @@ abstract class TestWidgetsFlutterBinding extends BindingBase
});
}

/// Convert the given point from the global coordinate system (as used by
/// pointer events from the device) to the coordinate system used by the
/// tests (an 800 by 600 window).
/// Convert the given point from the global coordinate space to the local
/// one.
///
/// For definitions for coordinate spaces, see [TestWidgetsFlutterBinding].
Offset globalToLocal(Offset point) => point;

/// Convert the given point from the coordinate system used by the tests (an
/// 800 by 600 window) to the global coordinate system (as used by pointer
/// events from the device).
/// Convert the given point from the local coordinate space to the global
/// one.
///
/// For definitions for coordinate spaces, see [TestWidgetsFlutterBinding].
Offset localToGlobal(Offset point) => point;

/// The source of the current pointer event.
///
/// The [pointerEventSource] is set as the `source` parameter of
/// [handlePointerEventForSource] and can be used in the immediate enclosing
/// [dispatchEvent].
///
/// When [handlePointerEvent] is called directly, [pointerEventSource]
/// is [TestBindingEventSource.device].
TestBindingEventSource get pointerEventSource => _pointerEventSource;
TestBindingEventSource _pointerEventSource = TestBindingEventSource.device;

/// Dispatch an event to the targets found by a hit test on its position,
/// and remember its source as [pointerEventSource].
///
/// This method sets [pointerEventSource] to `source`, runs
/// This method sets [pointerEventSource] to `source`, forwards the call to
/// [handlePointerEvent], then resets [pointerEventSource] to the previous
/// value.
///
/// If `source` is [TestBindingEventSource.device], then the `event` is based
/// in the global coordinate space (for definitions for coordinate spaces,
/// see [TestWidgetsFlutterBinding]) and the event is likely triggered by the
/// user physically interacting with the screen during a live test on a real
/// device (see [LiveTestWidgetsFlutterBinding]).
///
/// If `source` is [TestBindingEventSource.test], then the `event` is based
/// in the local coordinate space and the event is likely triggered by
/// programatically simulated pointer events, such as:
///
/// * [WidgetController.tap] and alike methods, as well as directly using
/// [TestGesture]. They are usually used in
/// [AutomatedTestWidgetsFlutterBinding] but sometimes in live tests too.
/// * [WidgetController.timedDrag] and alike methods. They are usually used
/// in macrobenchmarks.
void handlePointerEventForSource(
PointerEvent event, {
TestBindingEventSource source = TestBindingEventSource.device,
Expand All @@ -482,7 +517,7 @@ abstract class TestWidgetsFlutterBinding extends BindingBase
/// to the previous value.
@protected
void withPointerEventSource(TestBindingEventSource source, VoidCallback task) {
final TestBindingEventSource previousSource = source;
final TestBindingEventSource previousSource = _pointerEventSource;
_pointerEventSource = source;
try {
task();
Expand Down Expand Up @@ -1497,11 +1532,15 @@ class LiveTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding {
/// Events dispatched by [TestGesture] are not affected by this.
HitTestDispatcher? deviceEventDispatcher;


/// Dispatch an event to the targets found by a hit test on its position.
///
/// Apart from forwarding the event to [GestureBinding.dispatchEvent],
/// This also paint all events that's down on the screen.
/// If the [pointerEventSource] is [TestBindingEventSource.test], then
/// the event is forwarded to [GestureBinding.dispatchEvent] as usual;
/// additionally, down pointers are painted on the screen.
///
/// If the [pointerEventSource] is [TestBindingEventSource.device], then
/// the event, after being transformed to the local coordinate system, is
/// forwarded to [deviceEventDispatcher].
@override
void handlePointerEvent(PointerEvent event) {
switch (pointerEventSource) {
Expand All @@ -1523,8 +1562,12 @@ class LiveTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding {
break;
case TestBindingEventSource.device:
if (deviceEventDispatcher != null) {
// The pointer events received with this source has a global position
// (see [handlePointerEventForSource]). Transform it to the local
// coordinate space used by the testing widgets.
final PointerEvent localEvent = event.copyWith(position: globalToLocal(event.position));
withPointerEventSource(TestBindingEventSource.device,
() => super.handlePointerEvent(event)
() => super.handlePointerEvent(localEvent)
);
}
break;
Expand All @@ -1538,9 +1581,10 @@ class LiveTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding {
super.dispatchEvent(event, hitTestResult);
break;
case TestBindingEventSource.device:
assert(hitTestResult != null);
assert(hitTestResult != null || event is PointerAddedEvent || event is PointerRemovedEvent);
assert(deviceEventDispatcher != null);
deviceEventDispatcher!.dispatchEvent(event, hitTestResult!);
if (hitTestResult != null)
deviceEventDispatcher!.dispatchEvent(event, hitTestResult);
break;
}
}
Expand Down Expand Up @@ -1775,15 +1819,6 @@ class _LiveTestRenderView extends RenderView {
onNeedPaint();
}

@override
bool hitTest(HitTestResult result, { required Offset position }) {
final Matrix4 transform = configuration.toHitTestMatrix();
final 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);
Expand Down
28 changes: 20 additions & 8 deletions packages/flutter_test/lib/src/widget_tester.dart
Expand Up @@ -59,6 +59,21 @@ export 'package:test_api/test_api.dart' hide
/// Signature for callback to [testWidgets] and [benchmarkWidgets].
typedef WidgetTesterCallback = Future<void> Function(WidgetTester widgetTester);

// Return the last element that satisifes `test`, or return null if not found.
E? _lastWhereOrNull<E>(Iterable<E> list, bool Function(E) test) {
late E result;
bool foundMatching = false;
for (final E element in list) {
if (test(element)) {
result = element;
foundMatching = true;
}
}
if (foundMatching)
return result;
return null;
}

/// Runs the [callback] inside the Flutter test environment.
///
/// Use this function for testing custom [StatelessWidget]s and
Expand Down Expand Up @@ -800,15 +815,12 @@ class WidgetTester extends WidgetController implements HitTestDispatcher, Ticker
.map((HitTestEntry candidate) => candidate.target)
.whereType<RenderObject>()
.first;
final Element? innerTargetElement = collectAllElementsFrom(
binding.renderViewElement!,
skipOffstage: true,
).cast<Element?>().lastWhere(
(Element? element) => element!.renderObject == innerTarget,
orElse: () => null,
final Element? innerTargetElement = _lastWhereOrNull(
collectAllElementsFrom(binding.renderViewElement!, skipOffstage: true),
(Element element) => element.renderObject == innerTarget,
);
if (innerTargetElement == null) {
printToConsole('No widgets found at ${binding.globalToLocal(event.position)}.');
printToConsole('No widgets found at ${event.position}.');
return;
}
final List<Element> candidates = <Element>[];
Expand All @@ -821,7 +833,7 @@ class WidgetTester extends WidgetController implements HitTestDispatcher, Ticker
int numberOfWithTexts = 0;
int numberOfTypes = 0;
int totalNumber = 0;
printToConsole('Some possible finders for the widgets at ${binding.globalToLocal(event.position)}:');
printToConsole('Some possible finders for the widgets at ${event.position}:');
for (final Element element in candidates) {
if (totalNumber > 13) // an arbitrary number of finders that feels useful without being overwhelming
break;
Expand Down

0 comments on commit cd78190

Please sign in to comment.