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.
This commit is contained in:
parent
4ddaa13d01
commit
e3da1bd7aa
@ -167,7 +167,7 @@ Future<void> main() async {
|
||||
} else if (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 {
|
||||
final String firstComment = _wrapString('Represents the location of the '
|
||||
'"${entry.commentName}" key on a generalized keyboard.');
|
||||
final String otherComments = _wrapString('See the function '
|
||||
'[RawKeyEvent.physicalKey] for more information.');
|
||||
'[KeyEvent.physical] for more information.');
|
||||
definitions.write('''
|
||||
|
||||
$firstComment ///
|
||||
@ -67,7 +67,7 @@ $otherComments static const PhysicalKeyboardKey ${entry.constantName} = Physica
|
||||
final StringBuffer definitions = StringBuffer();
|
||||
void printKey(int flutterId, String constantName, String commentName, {String? otherComments}) {
|
||||
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('''
|
||||
|
||||
$firstComment ///
|
||||
|
@ -11,7 +11,7 @@ void main() {
|
||||
* 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);
|
||||
|
||||
await tester.pumpFrames(
|
||||
@ -40,8 +40,9 @@ void main() {
|
||||
const Duration(milliseconds: 100),
|
||||
);
|
||||
|
||||
final Widget display = await builder.display();
|
||||
await tester.binding.setSurfaceSize(builder.sheetSize());
|
||||
// This test verifies deprecated methods.
|
||||
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 expectLater(find.byWidget(display), matchesGoldenFile('test.animation_sheet_builder.records.png'));
|
||||
@ -57,12 +58,80 @@ void main() {
|
||||
const Duration(milliseconds: 200),
|
||||
);
|
||||
|
||||
final Widget display = await builder.display();
|
||||
await tester.binding.setSurfaceSize(builder.sheetSize(maxWidth: 80));
|
||||
// This test verifies deprecated methods.
|
||||
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 expectLater(find.byWidget(display), matchesGoldenFile('test.animation_sheet_builder.wraps.png'));
|
||||
}, 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
|
||||
|
62
packages/flutter/test/animation/live_binding_test.dart
Normal file
62
packages/flutter/test/animation/live_binding_test.dart
Normal file
@ -0,0 +1,62 @@
|
||||
// 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() {
|
||||
),
|
||||
), const Duration(seconds: 2));
|
||||
|
||||
tester.binding.setSurfaceSize(animationSheet.sheetSize());
|
||||
|
||||
final Widget display = await animationSheet.display();
|
||||
await tester.pumpWidget(display);
|
||||
|
||||
await expectLater(
|
||||
find.byWidget(display),
|
||||
await animationSheet.collate(20),
|
||||
matchesGoldenFile('material.circular_progress_indicator.indeterminate.png'),
|
||||
);
|
||||
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/42767
|
||||
|
@ -5,6 +5,7 @@
|
||||
import 'dart:math' as math;
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
@ -22,10 +23,8 @@ import 'package:flutter/widgets.dart';
|
||||
/// * Create an instance of this class.
|
||||
/// * Pump frames that render the target widget wrapped in [record]. Every frame
|
||||
/// that has `recording` being true will be recorded.
|
||||
/// * Adjust the size of the test viewport to the [sheetSize] (see the
|
||||
/// documentation of [sheetSize] for more information).
|
||||
/// * Pump a frame that renders [display], which shows all recorded frames in an
|
||||
/// animation sheet, and can be matched against the golden test.
|
||||
/// * Acquire the output image with [collate] and compare against the golden
|
||||
/// file.
|
||||
///
|
||||
/// {@tool snippet}
|
||||
/// The following example shows how to record an animation sheet of an [InkWell]
|
||||
@ -67,16 +66,9 @@ import 'package:flutter/widgets.dart';
|
||||
/// recording: true,
|
||||
/// ), const Duration(seconds: 1));
|
||||
///
|
||||
/// // Adjust view port size
|
||||
/// tester.binding.setSurfaceSize(animationSheet.sheetSize());
|
||||
///
|
||||
/// // Display
|
||||
/// final Widget display = await animationSheet.display();
|
||||
/// await tester.pumpWidget(display);
|
||||
///
|
||||
/// // Compare against golden file
|
||||
/// await expectLater(
|
||||
/// find.byWidget(display),
|
||||
/// animationSheet.collate(800),
|
||||
/// matchesGoldenFile('inkwell.press.animation.png'),
|
||||
/// );
|
||||
/// }, skip: isBrowser); // Animation sheet does not support browser https://github.com/flutter/flutter/issues/56001
|
||||
@ -91,7 +83,14 @@ class AnimationSheetBuilder {
|
||||
///
|
||||
/// The [frameSize] is a tight constraint for the child to be recorded, and must not
|
||||
/// be null.
|
||||
AnimationSheetBuilder({required this.frameSize}) : assert(frameSize != null);
|
||||
///
|
||||
/// The [allLayers] controls whether to record elements drawn out of the subtree,
|
||||
/// and defaults to false.
|
||||
AnimationSheetBuilder({
|
||||
required this.frameSize,
|
||||
this.allLayers = false,
|
||||
}) : assert(!kIsWeb), // Does not support Web. See [AnimationSheetBuilder].
|
||||
assert(frameSize != null);
|
||||
|
||||
/// The size of the child to be recorded.
|
||||
///
|
||||
@ -99,6 +98,22 @@ class AnimationSheetBuilder {
|
||||
/// fixed throughout the building session.
|
||||
final Size frameSize;
|
||||
|
||||
/// Whether the captured image comes from the entire tree, or only the
|
||||
/// subtree of [record].
|
||||
///
|
||||
/// If [allLayers] is false, then the [record] widget will capture the image
|
||||
/// composited by its subtree. If [allLayers] is true, then the [record] will
|
||||
/// capture the entire tree composited and clipped by [record]'s region.
|
||||
///
|
||||
/// The two modes are identical if there is nothing in front of [record].
|
||||
/// But in rare cases, what needs to be captured has to be rendered out of
|
||||
/// [record]'s subtree in its front. By setting [allLayers] to true, [record]
|
||||
/// captures everything within its region even if drawn outside of its
|
||||
/// subtree.
|
||||
///
|
||||
/// Defaults to false.
|
||||
final bool allLayers;
|
||||
|
||||
final List<Future<ui.Image>> _recordedFrames = <Future<ui.Image>>[];
|
||||
Future<List<ui.Image>> get _frames async {
|
||||
final List<ui.Image> frames = await Future.wait<ui.Image>(_recordedFrames, eagerError: true);
|
||||
@ -139,6 +154,7 @@ class AnimationSheetBuilder {
|
||||
return _AnimationSheetRecorder(
|
||||
key: key,
|
||||
size: frameSize,
|
||||
allLayers: allLayers,
|
||||
handleRecorded: recording ? _recordedFrames.add : null,
|
||||
child: child,
|
||||
);
|
||||
@ -159,6 +175,81 @@ class AnimationSheetBuilder {
|
||||
/// The `key` is applied to the root widget.
|
||||
///
|
||||
/// This method can only be called if at least one frame has been recorded.
|
||||
///
|
||||
/// The [display] is the legacy way of acquiring the output for comparison.
|
||||
/// It is not recommended because it requires more boilerplate, and produces
|
||||
/// a much large image than necessary: each pixel is rendered in 3x3 pixels
|
||||
/// without higher definition. Use [collate] instead.
|
||||
///
|
||||
/// Using this way includes the following steps:
|
||||
///
|
||||
/// * Create an instance of this class.
|
||||
/// * Pump frames that render the target widget wrapped in [record]. Every frame
|
||||
/// that has `recording` being true will be recorded.
|
||||
/// * Adjust the size of the test viewport to the [sheetSize] (see the
|
||||
/// documentation of [sheetSize] for more information).
|
||||
/// * Pump a frame that renders [display], which shows all recorded frames in an
|
||||
/// animation sheet, and can be matched against the golden test.
|
||||
///
|
||||
/// {@tool snippet}
|
||||
/// The following example shows how to record an animation sheet of an [InkWell]
|
||||
/// being pressed then released.
|
||||
///
|
||||
/// ```dart
|
||||
/// testWidgets('Inkwell animation sheet', (WidgetTester tester) async {
|
||||
/// // Create instance
|
||||
/// final AnimationSheetBuilder animationSheet = AnimationSheetBuilder(frameSize: const Size(48, 24));
|
||||
///
|
||||
/// final Widget target = Material(
|
||||
/// child: Directionality(
|
||||
/// textDirection: TextDirection.ltr,
|
||||
/// child: InkWell(
|
||||
/// splashColor: Colors.blue,
|
||||
/// onTap: () {},
|
||||
/// ),
|
||||
/// ),
|
||||
/// );
|
||||
///
|
||||
/// // Optional: setup before recording (`recording` is false)
|
||||
/// await tester.pumpWidget(animationSheet.record(
|
||||
/// target,
|
||||
/// recording: false,
|
||||
/// ));
|
||||
///
|
||||
/// final TestGesture gesture = await tester.startGesture(tester.getCenter(find.byType(InkWell)));
|
||||
///
|
||||
/// // Start recording (`recording` is true)
|
||||
/// await tester.pumpFrames(animationSheet.record(
|
||||
/// target,
|
||||
/// recording: true,
|
||||
/// ), const Duration(seconds: 1));
|
||||
///
|
||||
/// await gesture.up();
|
||||
///
|
||||
/// await tester.pumpFrames(animationSheet.record(
|
||||
/// target,
|
||||
/// recording: true,
|
||||
/// ), const Duration(seconds: 1));
|
||||
///
|
||||
/// // Adjust view port size
|
||||
/// tester.binding.setSurfaceSize(animationSheet.sheetSize());
|
||||
///
|
||||
/// // Display
|
||||
/// final Widget display = await animationSheet.display();
|
||||
/// await tester.pumpWidget(display);
|
||||
///
|
||||
/// // Compare against golden file
|
||||
/// await expectLater(
|
||||
/// find.byWidget(display),
|
||||
/// matchesGoldenFile('inkwell.press.animation.png'),
|
||||
/// );
|
||||
/// }, skip: isBrowser); // Animation sheet does not support browser https://github.com/flutter/flutter/issues/56001
|
||||
/// ```
|
||||
/// {@end-tool}
|
||||
@Deprecated(
|
||||
'Use AnimationSheetBuilder.collate instead. '
|
||||
'This feature was deprecated after v2.3.0-13.0.pre.',
|
||||
)
|
||||
Future<Widget> display({Key? key}) async {
|
||||
assert(_recordedFrames.isNotEmpty);
|
||||
final List<ui.Image> frames = await _frames;
|
||||
@ -176,6 +267,16 @@ class AnimationSheetBuilder {
|
||||
);
|
||||
}
|
||||
|
||||
/// Returns an result image by putting all frames together in a table.
|
||||
///
|
||||
/// This method returns a table of captured frames, `cellsPerRow` images
|
||||
/// per row, from left to right, top to bottom.
|
||||
///
|
||||
/// An example of using this method can be found at [AnimationSheetBuilder].
|
||||
Future<ui.Image> collate(int cellsPerRow) async {
|
||||
return _collateFrames(await _frames, frameSize, cellsPerRow);
|
||||
}
|
||||
|
||||
/// Returns the smallest size that can contain all recorded frames.
|
||||
///
|
||||
/// This is used to adjust the viewport during unit tests, i.e. the size of
|
||||
@ -194,6 +295,10 @@ class AnimationSheetBuilder {
|
||||
/// The `maxWidth` defaults to the width of the default viewport, 800.0.
|
||||
///
|
||||
/// This method can only be called if at least one frame has been recorded.
|
||||
@Deprecated(
|
||||
'The `sheetSize` is only useful for `display`, which should be migrated to `collate`. '
|
||||
'This feature was deprecated after v2.3.0-13.0.pre.',
|
||||
)
|
||||
Size sheetSize({double maxWidth = _kDefaultTestViewportWidth}) {
|
||||
assert(_recordedFrames.isNotEmpty);
|
||||
final int cellsPerRow = (maxWidth / frameSize.width).floor();
|
||||
@ -213,12 +318,14 @@ class _AnimationSheetRecorder extends StatefulWidget {
|
||||
this.handleRecorded,
|
||||
required this.child,
|
||||
required this.size,
|
||||
required this.allLayers,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
final _RecordedHandler? handleRecorded;
|
||||
final Widget child;
|
||||
final Size size;
|
||||
final bool allLayers;
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _AnimationSheetRecorderState();
|
||||
@ -229,8 +336,12 @@ class _AnimationSheetRecorderState extends State<_AnimationSheetRecorder> {
|
||||
|
||||
void _record(Duration duration) {
|
||||
assert(widget.handleRecorded != null);
|
||||
final RenderRepaintBoundary boundary = boundaryKey.currentContext!.findRenderObject()! as RenderRepaintBoundary;
|
||||
widget.handleRecorded!(boundary.toImage());
|
||||
final _RenderRootableRepaintBoundary boundary = boundaryKey.currentContext!.findRenderObject()! as _RenderRootableRepaintBoundary;
|
||||
if (widget.allLayers) {
|
||||
widget.handleRecorded!(boundary.allLayersToImage());
|
||||
} else {
|
||||
widget.handleRecorded!(boundary.toImage());
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
@ -239,7 +350,7 @@ class _AnimationSheetRecorderState extends State<_AnimationSheetRecorder> {
|
||||
alignment: Alignment.topLeft,
|
||||
child: SizedBox.fromSize(
|
||||
size: widget.size,
|
||||
child: RepaintBoundary(
|
||||
child: _RootableRepaintBoundary(
|
||||
key: boundaryKey,
|
||||
child: _PostFrameCallbacker(
|
||||
callback: widget.handleRecorded == null ? null : _record,
|
||||
@ -311,6 +422,27 @@ class _RenderPostFrameCallbacker extends RenderProxyBox {
|
||||
}
|
||||
}
|
||||
|
||||
Future<ui.Image> _collateFrames(List<ui.Image> frames, Size frameSize, int cellsPerRow) async {
|
||||
final int rowNum = (frames.length / cellsPerRow).ceil();
|
||||
|
||||
final ui.PictureRecorder recorder = ui.PictureRecorder();
|
||||
final Canvas canvas = Canvas(
|
||||
recorder,
|
||||
Rect.fromLTWH(0, 0, frameSize.width * cellsPerRow, frameSize.height * rowNum),
|
||||
);
|
||||
for (int i = 0; i < frames.length; i += 1) {
|
||||
canvas.drawImage(
|
||||
frames[i],
|
||||
Offset(frameSize.width * (i % cellsPerRow), frameSize.height * (i / cellsPerRow).floor()),
|
||||
Paint(),
|
||||
);
|
||||
}
|
||||
return recorder.endRecording().toImage(
|
||||
(frameSize.width * cellsPerRow).toInt(),
|
||||
(frameSize.height * rowNum).toInt(),
|
||||
);
|
||||
}
|
||||
|
||||
// Layout children in a grid of fixed-sized cells.
|
||||
//
|
||||
// The sheet fills up as much space as the parent allows. The cells are
|
||||
@ -351,3 +483,33 @@ class _CellSheet extends StatelessWidget {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class _RenderRootableRepaintBoundary extends RenderRepaintBoundary {
|
||||
// Like [toImage], but captures an image of all layers (composited by
|
||||
// RenderView and its children) clipped by the region of this object.
|
||||
Future<ui.Image> allLayersToImage() {
|
||||
final TransformLayer rootLayer = _rootLayer();
|
||||
final Matrix4 rootTransform = (rootLayer.transform ?? Matrix4.identity()).clone();
|
||||
final Matrix4 transform = rootTransform.multiplied(getTransformTo(null));
|
||||
final Rect rect = MatrixUtils.transformRect(transform, Offset.zero & size);
|
||||
// The scale was used to fit the actual device. Revert it since the target
|
||||
// is the logical display. Take transform[0] as the scale.
|
||||
return rootLayer.toImage(rect, pixelRatio: 1 / transform[0]);
|
||||
}
|
||||
|
||||
TransformLayer _rootLayer() {
|
||||
Layer layer = this.layer!;
|
||||
while (layer.parent != null)
|
||||
layer = layer.parent!;
|
||||
return layer as TransformLayer;
|
||||
}
|
||||
}
|
||||
|
||||
// A [RepaintBoundary], except that its render object has a `fullscreenToImage` method.
|
||||
class _RootableRepaintBoundary extends SingleChildRenderObjectWidget {
|
||||
/// Creates a widget that isolates repaints.
|
||||
const _RootableRepaintBoundary({ Key? key, Widget? child }) : super(key: key, child: child);
|
||||
|
||||
@override
|
||||
_RenderRootableRepaintBoundary createRenderObject(BuildContext context) => _RenderRootableRepaintBoundary();
|
||||
}
|
||||
|
@ -457,22 +457,35 @@ abstract class TestWidgetsFlutterBinding extends BindingBase
|
||||
/// events from the device).
|
||||
Offset localToGlobal(Offset point) => point;
|
||||
|
||||
// The source of the current pointer event.
|
||||
//
|
||||
// The [pointerEventSource] is set as the `source` parameter of
|
||||
// [handlePointerEvent] and can be used in the immediate enclosing
|
||||
// [dispatchEvent].
|
||||
/// 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].
|
||||
TestBindingEventSource get pointerEventSource => _pointerEventSource;
|
||||
TestBindingEventSource _pointerEventSource = TestBindingEventSource.device;
|
||||
|
||||
@override
|
||||
void handlePointerEvent(
|
||||
/// 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
|
||||
/// [handlePointerEvent], then resets [pointerEventSource] to the previous
|
||||
/// value.
|
||||
void handlePointerEventForSource(
|
||||
PointerEvent event, {
|
||||
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;
|
||||
_pointerEventSource = source;
|
||||
try {
|
||||
super.handlePointerEvent(event);
|
||||
task();
|
||||
} finally {
|
||||
_pointerEventSource = previousSource;
|
||||
}
|
||||
@ -1490,11 +1503,8 @@ class LiveTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding {
|
||||
/// Apart from forwarding the event to [GestureBinding.dispatchEvent],
|
||||
/// This also paint all events that's down on the screen.
|
||||
@override
|
||||
void handlePointerEvent(
|
||||
PointerEvent event, {
|
||||
TestBindingEventSource source = TestBindingEventSource.device,
|
||||
}) {
|
||||
switch (source) {
|
||||
void handlePointerEvent(PointerEvent event) {
|
||||
switch (pointerEventSource) {
|
||||
case TestBindingEventSource.test:
|
||||
final _LiveTestPointerRecord? record = _liveTestRenderView._pointers[event.pointer];
|
||||
if (record != null) {
|
||||
@ -1509,18 +1519,21 @@ class LiveTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding {
|
||||
);
|
||||
_handleViewNeedsPaint();
|
||||
}
|
||||
super.handlePointerEvent(event, source: TestBindingEventSource.test);
|
||||
super.handlePointerEvent(event);
|
||||
break;
|
||||
case TestBindingEventSource.device:
|
||||
if (deviceEventDispatcher != null)
|
||||
super.handlePointerEvent(event, source: TestBindingEventSource.device);
|
||||
if (deviceEventDispatcher != null) {
|
||||
withPointerEventSource(TestBindingEventSource.device,
|
||||
() => super.handlePointerEvent(event)
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispatchEvent(PointerEvent event, HitTestResult? hitTestResult) {
|
||||
switch (_pointerEventSource) {
|
||||
switch (pointerEventSource) {
|
||||
case TestBindingEventSource.test:
|
||||
super.dispatchEvent(event, hitTestResult);
|
||||
break;
|
||||
|
@ -550,7 +550,7 @@ class WidgetTester extends WidgetController implements HitTestDispatcher, Ticker
|
||||
// Flush all past events
|
||||
handleTimeStampDiff.add(-timeDiff);
|
||||
for (final PointerEvent event in record.events) {
|
||||
binding.handlePointerEvent(event, source: TestBindingEventSource.test);
|
||||
binding.handlePointerEventForSource(event, source: TestBindingEventSource.test);
|
||||
}
|
||||
} else {
|
||||
await binding.pump();
|
||||
@ -559,7 +559,7 @@ class WidgetTester extends WidgetController implements HitTestDispatcher, Ticker
|
||||
binding.clock.now().difference(startTime) - record.timeDelay,
|
||||
);
|
||||
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
|
||||
@override
|
||||
Future<void> sendEventToBinding(PointerEvent event) {
|
||||
return TestAsyncUtils.guard<void>(() async {
|
||||
binding.handlePointerEvent(event, source: TestBindingEventSource.test);
|
||||
binding.handlePointerEventForSource(event, source: TestBindingEventSource.test);
|
||||
});
|
||||
}
|
||||
|
||||
|
100
packages/flutter_test/test/widget_tester_live_device_test.dart
Normal file
100
packages/flutter_test/test/widget_tester_live_device_test.dart
Normal file
@ -0,0 +1,100 @@
|
||||
// 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() {
|
||||
|
||||
expect(flutterErrorDetails.exception, isA<AssertionError>());
|
||||
expect((flutterErrorDetails.exception as AssertionError).message, 'A Timer is still pending even after the widget tree was disposed.');
|
||||
expect(binding.inTest, true);
|
||||
binding.postTest();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user