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 {
} 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
......
// 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);
});
}
......
// 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();
});
});
}
......
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