Unverified Commit e3da1bd7 authored by Tong Mu's avatar Tong Mu Committed by GitHub

Test WidgetTester handling test pointers (#83337)

Adds tests to the following behaviors, which have existed without tests:

- When tapping during live testing, a message is printed with widgets that contain the tap location.
- When tapping during live testing, a mark is displayed on screen on the tap location.
parent 4ddaa13d
...@@ -167,7 +167,7 @@ Future<void> main() async { ...@@ -167,7 +167,7 @@ Future<void> main() async {
} else if (delays.last < delay) { } else if (delays.last < delay) {
delays.last = delay; delays.last = delay;
} }
tester.binding.handlePointerEvent(event, source: TestBindingEventSource.test); tester.binding.handlePointerEventForSource(event, source: TestBindingEventSource.test);
} }
} }
......
...@@ -42,7 +42,7 @@ class KeyboardKeysCodeGenerator extends BaseCodeGenerator { ...@@ -42,7 +42,7 @@ class KeyboardKeysCodeGenerator extends BaseCodeGenerator {
final String firstComment = _wrapString('Represents the location of the ' final String firstComment = _wrapString('Represents the location of the '
'"${entry.commentName}" key on a generalized keyboard.'); '"${entry.commentName}" key on a generalized keyboard.');
final String otherComments = _wrapString('See the function ' final String otherComments = _wrapString('See the function '
'[RawKeyEvent.physicalKey] for more information.'); '[KeyEvent.physical] for more information.');
definitions.write(''' definitions.write('''
$firstComment /// $firstComment ///
...@@ -67,7 +67,7 @@ $otherComments static const PhysicalKeyboardKey ${entry.constantName} = Physica ...@@ -67,7 +67,7 @@ $otherComments static const PhysicalKeyboardKey ${entry.constantName} = Physica
final StringBuffer definitions = StringBuffer(); final StringBuffer definitions = StringBuffer();
void printKey(int flutterId, String constantName, String commentName, {String? otherComments}) { void printKey(int flutterId, String constantName, String commentName, {String? otherComments}) {
final String firstComment = _wrapString('Represents the logical "$commentName" key on the keyboard.'); final String firstComment = _wrapString('Represents the logical "$commentName" key on the keyboard.');
otherComments ??= _wrapString('See the function [RawKeyEvent.logicalKey] for more information.'); otherComments ??= _wrapString('See the function [KeyEvent.logical] for more information.');
definitions.write(''' definitions.write('''
$firstComment /// $firstComment ///
......
...@@ -11,7 +11,7 @@ void main() { ...@@ -11,7 +11,7 @@ void main() {
* because [matchesGoldenFile] does not use Skia Gold in its native package. * because [matchesGoldenFile] does not use Skia Gold in its native package.
*/ */
testWidgets('correctly records frames', (WidgetTester tester) async { testWidgets('correctly records frames using display', (WidgetTester tester) async {
final AnimationSheetBuilder builder = AnimationSheetBuilder(frameSize: _DecuplePixels.size); final AnimationSheetBuilder builder = AnimationSheetBuilder(frameSize: _DecuplePixels.size);
await tester.pumpFrames( await tester.pumpFrames(
...@@ -40,8 +40,9 @@ void main() { ...@@ -40,8 +40,9 @@ void main() {
const Duration(milliseconds: 100), const Duration(milliseconds: 100),
); );
final Widget display = await builder.display(); // This test verifies deprecated methods.
await tester.binding.setSurfaceSize(builder.sheetSize()); final Widget display = await builder.display(); // ignore: deprecated_member_use
await tester.binding.setSurfaceSize(builder.sheetSize()); // ignore: deprecated_member_use
await tester.pumpWidget(display); await tester.pumpWidget(display);
await expectLater(find.byWidget(display), matchesGoldenFile('test.animation_sheet_builder.records.png')); await expectLater(find.byWidget(display), matchesGoldenFile('test.animation_sheet_builder.records.png'));
...@@ -57,12 +58,80 @@ void main() { ...@@ -57,12 +58,80 @@ void main() {
const Duration(milliseconds: 200), const Duration(milliseconds: 200),
); );
final Widget display = await builder.display(); // This test verifies deprecated methods.
await tester.binding.setSurfaceSize(builder.sheetSize(maxWidth: 80)); final Widget display = await builder.display(); // ignore: deprecated_member_use
await tester.binding.setSurfaceSize(builder.sheetSize(maxWidth: 80)); // ignore: deprecated_member_use
await tester.pumpWidget(display); await tester.pumpWidget(display);
await expectLater(find.byWidget(display), matchesGoldenFile('test.animation_sheet_builder.wraps.png')); await expectLater(find.byWidget(display), matchesGoldenFile('test.animation_sheet_builder.wraps.png'));
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/42767 }, skip: isBrowser); // https://github.com/flutter/flutter/issues/42767
testWidgets('correctly records frames using collate', (WidgetTester tester) async {
final AnimationSheetBuilder builder = AnimationSheetBuilder(frameSize: _DecuplePixels.size);
await tester.pumpFrames(
builder.record(
const _DecuplePixels(Duration(seconds: 1)),
),
const Duration(milliseconds: 200),
const Duration(milliseconds: 100),
);
await tester.pumpFrames(
builder.record(
const _DecuplePixels(Duration(seconds: 1)),
recording: false,
),
const Duration(milliseconds: 200),
const Duration(milliseconds: 100),
);
await tester.pumpFrames(
builder.record(
const _DecuplePixels(Duration(seconds: 1)),
recording: true,
),
const Duration(milliseconds: 400),
const Duration(milliseconds: 100),
);
await expectLater(
builder.collate(5),
matchesGoldenFile('test.animation_sheet_builder.collate.png'),
);
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/42767
testWidgets('use allLayers to record out-of-subtree contents', (WidgetTester tester) async {
final AnimationSheetBuilder builder = AnimationSheetBuilder(
frameSize: const Size(8, 2),
allLayers: true,
);
// The `record` (sized 8, 2) is placed on top of `_DecuplePixels`
// (sized 12, 3), aligned at its top left.
await tester.pumpFrames(
Directionality(
textDirection: TextDirection.ltr,
child: Stack(
children: <Widget>[
const _DecuplePixels(Duration(seconds: 1)),
Align(
alignment: Alignment.topLeft,
child: builder.record(Container()),
),
],
),
),
const Duration(milliseconds: 600),
const Duration(milliseconds: 100),
);
await expectLater(
builder.collate(5),
matchesGoldenFile('test.animation_sheet_builder.out_of_tree.png'),
);
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/42767
} }
// An animation of a yellow pixel moving from left to right, in a container of // An animation of a yellow pixel moving from left to right, in a container of
......
// Copyright 2014 The Flutter 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/material.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
/*
* Here lies golden tests for packages/flutter_test/lib/src/binding.dart
* because [matchesGoldenFile] does not use Skia Gold in its native package.
*/
LiveTestWidgetsFlutterBinding();
testWidgets('Should show event indicator for pointer events', (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: GestureDetector(
onTap: () {},
child: const Text('Test'),
),
),
),
),
),
);
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(Text)) + 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 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.png'),
);
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/42767
}
...@@ -772,13 +772,8 @@ void main() { ...@@ -772,13 +772,8 @@ void main() {
), ),
), const Duration(seconds: 2)); ), const Duration(seconds: 2));
tester.binding.setSurfaceSize(animationSheet.sheetSize());
final Widget display = await animationSheet.display();
await tester.pumpWidget(display);
await expectLater( await expectLater(
find.byWidget(display), await animationSheet.collate(20),
matchesGoldenFile('material.circular_progress_indicator.indeterminate.png'), matchesGoldenFile('material.circular_progress_indicator.indeterminate.png'),
); );
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/42767 }, skip: isBrowser); // https://github.com/flutter/flutter/issues/42767
......
...@@ -457,22 +457,35 @@ abstract class TestWidgetsFlutterBinding extends BindingBase ...@@ -457,22 +457,35 @@ abstract class TestWidgetsFlutterBinding extends BindingBase
/// events from the device). /// events from the device).
Offset localToGlobal(Offset point) => point; Offset localToGlobal(Offset point) => point;
// The source of the current pointer event. /// The source of the current pointer event.
// ///
// The [pointerEventSource] is set as the `source` parameter of /// The [pointerEventSource] is set as the `source` parameter of
// [handlePointerEvent] and can be used in the immediate enclosing /// [handlePointerEventForSource] and can be used in the immediate enclosing
// [dispatchEvent]. /// [dispatchEvent].
TestBindingEventSource get pointerEventSource => _pointerEventSource;
TestBindingEventSource _pointerEventSource = TestBindingEventSource.device; TestBindingEventSource _pointerEventSource = TestBindingEventSource.device;
@override /// Dispatch an event to the targets found by a hit test on its position,
void handlePointerEvent( /// and remember its source as [pointerEventSource].
///
/// This method sets [pointerEventSource] to `source`, runs
/// [handlePointerEvent], then resets [pointerEventSource] to the previous
/// value.
void handlePointerEventForSource(
PointerEvent event, { PointerEvent event, {
TestBindingEventSource source = TestBindingEventSource.device, TestBindingEventSource source = TestBindingEventSource.device,
}) { }) {
withPointerEventSource(source, () => handlePointerEvent(event));
}
/// Sets [pointerEventSource] to `source`, runs `task`, then resets `source`
/// to the previous value.
@protected
void withPointerEventSource(TestBindingEventSource source, VoidCallback task) {
final TestBindingEventSource previousSource = source; final TestBindingEventSource previousSource = source;
_pointerEventSource = source; _pointerEventSource = source;
try { try {
super.handlePointerEvent(event); task();
} finally { } finally {
_pointerEventSource = previousSource; _pointerEventSource = previousSource;
} }
...@@ -1490,11 +1503,8 @@ class LiveTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding { ...@@ -1490,11 +1503,8 @@ class LiveTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding {
/// Apart from forwarding the event to [GestureBinding.dispatchEvent], /// Apart from forwarding the event to [GestureBinding.dispatchEvent],
/// This also paint all events that's down on the screen. /// This also paint all events that's down on the screen.
@override @override
void handlePointerEvent( void handlePointerEvent(PointerEvent event) {
PointerEvent event, { switch (pointerEventSource) {
TestBindingEventSource source = TestBindingEventSource.device,
}) {
switch (source) {
case TestBindingEventSource.test: case TestBindingEventSource.test:
final _LiveTestPointerRecord? record = _liveTestRenderView._pointers[event.pointer]; final _LiveTestPointerRecord? record = _liveTestRenderView._pointers[event.pointer];
if (record != null) { if (record != null) {
...@@ -1509,18 +1519,21 @@ class LiveTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding { ...@@ -1509,18 +1519,21 @@ class LiveTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding {
); );
_handleViewNeedsPaint(); _handleViewNeedsPaint();
} }
super.handlePointerEvent(event, source: TestBindingEventSource.test); super.handlePointerEvent(event);
break; break;
case TestBindingEventSource.device: case TestBindingEventSource.device:
if (deviceEventDispatcher != null) if (deviceEventDispatcher != null) {
super.handlePointerEvent(event, source: TestBindingEventSource.device); withPointerEventSource(TestBindingEventSource.device,
() => super.handlePointerEvent(event)
);
}
break; break;
} }
} }
@override @override
void dispatchEvent(PointerEvent event, HitTestResult? hitTestResult) { void dispatchEvent(PointerEvent event, HitTestResult? hitTestResult) {
switch (_pointerEventSource) { switch (pointerEventSource) {
case TestBindingEventSource.test: case TestBindingEventSource.test:
super.dispatchEvent(event, hitTestResult); super.dispatchEvent(event, hitTestResult);
break; break;
......
...@@ -550,7 +550,7 @@ class WidgetTester extends WidgetController implements HitTestDispatcher, Ticker ...@@ -550,7 +550,7 @@ class WidgetTester extends WidgetController implements HitTestDispatcher, Ticker
// Flush all past events // Flush all past events
handleTimeStampDiff.add(-timeDiff); handleTimeStampDiff.add(-timeDiff);
for (final PointerEvent event in record.events) { for (final PointerEvent event in record.events) {
binding.handlePointerEvent(event, source: TestBindingEventSource.test); binding.handlePointerEventForSource(event, source: TestBindingEventSource.test);
} }
} else { } else {
await binding.pump(); await binding.pump();
...@@ -559,7 +559,7 @@ class WidgetTester extends WidgetController implements HitTestDispatcher, Ticker ...@@ -559,7 +559,7 @@ class WidgetTester extends WidgetController implements HitTestDispatcher, Ticker
binding.clock.now().difference(startTime) - record.timeDelay, binding.clock.now().difference(startTime) - record.timeDelay,
); );
for (final PointerEvent event in record.events) { for (final PointerEvent event in record.events) {
binding.handlePointerEvent(event, source: TestBindingEventSource.test); binding.handlePointerEventForSource(event, source: TestBindingEventSource.test);
} }
} }
} }
...@@ -788,7 +788,7 @@ class WidgetTester extends WidgetController implements HitTestDispatcher, Ticker ...@@ -788,7 +788,7 @@ class WidgetTester extends WidgetController implements HitTestDispatcher, Ticker
@override @override
Future<void> sendEventToBinding(PointerEvent event) { Future<void> sendEventToBinding(PointerEvent event) {
return TestAsyncUtils.guard<void>(() async { return TestAsyncUtils.guard<void>(() async {
binding.handlePointerEvent(event, source: TestBindingEventSource.test); binding.handlePointerEventForSource(event, source: TestBindingEventSource.test);
}); });
} }
......
// Copyright 2014 The Flutter 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/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
final _MockLiveTestWidgetsFlutterBinding binding = _MockLiveTestWidgetsFlutterBinding();
testWidgets('Should print message on pointer events', (WidgetTester tester) async {
final List<String?> printedMessages = <String?>[];
int invocations = 0;
await tester.pumpWidget(
MaterialApp(
home: Center(
child: GestureDetector(
onTap: () {
invocations++;
},
child: const Text('Test'),
),
),
),
);
final Size windowCenter = tester.binding.window.physicalSize /
tester.binding.window.devicePixelRatio /
2;
final double windowCenterX = windowCenter.width;
final double windowCenterY = windowCenter.height;
final Offset widgetCenter = tester.getRect(find.byType(Text)).center;
expect(widgetCenter.dx, windowCenterX);
expect(widgetCenter.dy, windowCenterY);
await binding.collectDebugPrints(printedMessages, () async {
await tester.tap(find.byType(Text));
});
await tester.pump();
expect(invocations, 0);
expect(printedMessages, equals('''
Some possible finders for the widgets at Offset(400.0, 300.0):
find.text('Test')
find.widgetWithText(RawGestureDetector, 'Test')
find.byType(GestureDetector)
find.byType(Center)
find.widgetWithText(IgnorePointer, 'Test')
find.byType(FadeTransition)
find.byType(FractionalTranslation)
find.byType(SlideTransition)
find.widgetWithText(PrimaryScrollController, 'Test')
find.widgetWithText(PageStorage, 'Test')
find.widgetWithText(Offstage, 'Test')
'''.trim().split('\n')));
printedMessages.clear();
await binding.collectDebugPrints(printedMessages, () async {
await tester.tapAt(const Offset(1, 1));
});
expect(printedMessages, equals('''
Some possible finders for the widgets at Offset(1.0, 1.0):
find.byType(MouseRegion)
find.byType(ExcludeSemantics)
find.byType(BlockSemantics)
find.byType(ModalBarrier)
find.byType(Overlay)
'''.trim().split('\n')));
});
}
class _MockLiveTestWidgetsFlutterBinding extends LiveTestWidgetsFlutterBinding {
@override
TestBindingEventSource get pointerEventSource => TestBindingEventSource.device;
List<String?>? _storeDebugPrints;
@override
DebugPrintCallback get debugPrintOverride {
return _storeDebugPrints == null
? super.debugPrintOverride
: ((String? message, { int? wrapWidth }) => _storeDebugPrints!.add(message));
}
// Execute `task` while redirecting [debugPrint] to appending to `store`.
Future<void> collectDebugPrints(List<String?>? store, AsyncValueGetter<void> task) async {
_storeDebugPrints = store;
try {
await task();
} finally {
_storeDebugPrints = null;
}
}
}
...@@ -759,6 +759,8 @@ void main() { ...@@ -759,6 +759,8 @@ void main() {
expect(flutterErrorDetails.exception, isA<AssertionError>()); expect(flutterErrorDetails.exception, isA<AssertionError>());
expect((flutterErrorDetails.exception as AssertionError).message, 'A Timer is still pending even after the widget tree was disposed.'); expect((flutterErrorDetails.exception as AssertionError).message, 'A Timer is still pending even after the widget tree was disposed.');
expect(binding.inTest, true);
binding.postTest();
}); });
}); });
} }
......
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