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) {
|
} 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 {
|
|||||||
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
|
|||||||
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() {
|
|||||||
* 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() {
|
|||||||
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() {
|
|||||||
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
|
||||||
|
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));
|
), 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
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
import 'dart:ui' as ui;
|
import 'dart:ui' as ui;
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/rendering.dart';
|
import 'package:flutter/rendering.dart';
|
||||||
import 'package:flutter/scheduler.dart';
|
import 'package:flutter/scheduler.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
@ -22,10 +23,8 @@ import 'package:flutter/widgets.dart';
|
|||||||
/// * Create an instance of this class.
|
/// * Create an instance of this class.
|
||||||
/// * Pump frames that render the target widget wrapped in [record]. Every frame
|
/// * Pump frames that render the target widget wrapped in [record]. Every frame
|
||||||
/// that has `recording` being true will be recorded.
|
/// that has `recording` being true will be recorded.
|
||||||
/// * Adjust the size of the test viewport to the [sheetSize] (see the
|
/// * Acquire the output image with [collate] and compare against the golden
|
||||||
/// documentation of [sheetSize] for more information).
|
/// file.
|
||||||
/// * 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}
|
/// {@tool snippet}
|
||||||
/// The following example shows how to record an animation sheet of an [InkWell]
|
/// The following example shows how to record an animation sheet of an [InkWell]
|
||||||
@ -67,16 +66,9 @@ import 'package:flutter/widgets.dart';
|
|||||||
/// recording: true,
|
/// recording: true,
|
||||||
/// ), const Duration(seconds: 1));
|
/// ), 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
|
/// // Compare against golden file
|
||||||
/// await expectLater(
|
/// await expectLater(
|
||||||
/// find.byWidget(display),
|
/// animationSheet.collate(800),
|
||||||
/// matchesGoldenFile('inkwell.press.animation.png'),
|
/// matchesGoldenFile('inkwell.press.animation.png'),
|
||||||
/// );
|
/// );
|
||||||
/// }, skip: isBrowser); // Animation sheet does not support browser https://github.com/flutter/flutter/issues/56001
|
/// }, 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
|
/// The [frameSize] is a tight constraint for the child to be recorded, and must not
|
||||||
/// be null.
|
/// 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.
|
/// The size of the child to be recorded.
|
||||||
///
|
///
|
||||||
@ -99,6 +98,22 @@ class AnimationSheetBuilder {
|
|||||||
/// fixed throughout the building session.
|
/// fixed throughout the building session.
|
||||||
final Size frameSize;
|
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>>[];
|
final List<Future<ui.Image>> _recordedFrames = <Future<ui.Image>>[];
|
||||||
Future<List<ui.Image>> get _frames async {
|
Future<List<ui.Image>> get _frames async {
|
||||||
final List<ui.Image> frames = await Future.wait<ui.Image>(_recordedFrames, eagerError: true);
|
final List<ui.Image> frames = await Future.wait<ui.Image>(_recordedFrames, eagerError: true);
|
||||||
@ -139,6 +154,7 @@ class AnimationSheetBuilder {
|
|||||||
return _AnimationSheetRecorder(
|
return _AnimationSheetRecorder(
|
||||||
key: key,
|
key: key,
|
||||||
size: frameSize,
|
size: frameSize,
|
||||||
|
allLayers: allLayers,
|
||||||
handleRecorded: recording ? _recordedFrames.add : null,
|
handleRecorded: recording ? _recordedFrames.add : null,
|
||||||
child: child,
|
child: child,
|
||||||
);
|
);
|
||||||
@ -159,6 +175,81 @@ class AnimationSheetBuilder {
|
|||||||
/// The `key` is applied to the root widget.
|
/// The `key` is applied to the root widget.
|
||||||
///
|
///
|
||||||
/// This method can only be called if at least one frame has been recorded.
|
/// 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 {
|
Future<Widget> display({Key? key}) async {
|
||||||
assert(_recordedFrames.isNotEmpty);
|
assert(_recordedFrames.isNotEmpty);
|
||||||
final List<ui.Image> frames = await _frames;
|
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.
|
/// 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
|
/// 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.
|
/// 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.
|
/// 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}) {
|
Size sheetSize({double maxWidth = _kDefaultTestViewportWidth}) {
|
||||||
assert(_recordedFrames.isNotEmpty);
|
assert(_recordedFrames.isNotEmpty);
|
||||||
final int cellsPerRow = (maxWidth / frameSize.width).floor();
|
final int cellsPerRow = (maxWidth / frameSize.width).floor();
|
||||||
@ -213,12 +318,14 @@ class _AnimationSheetRecorder extends StatefulWidget {
|
|||||||
this.handleRecorded,
|
this.handleRecorded,
|
||||||
required this.child,
|
required this.child,
|
||||||
required this.size,
|
required this.size,
|
||||||
|
required this.allLayers,
|
||||||
Key? key,
|
Key? key,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
final _RecordedHandler? handleRecorded;
|
final _RecordedHandler? handleRecorded;
|
||||||
final Widget child;
|
final Widget child;
|
||||||
final Size size;
|
final Size size;
|
||||||
|
final bool allLayers;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<StatefulWidget> createState() => _AnimationSheetRecorderState();
|
State<StatefulWidget> createState() => _AnimationSheetRecorderState();
|
||||||
@ -229,8 +336,12 @@ class _AnimationSheetRecorderState extends State<_AnimationSheetRecorder> {
|
|||||||
|
|
||||||
void _record(Duration duration) {
|
void _record(Duration duration) {
|
||||||
assert(widget.handleRecorded != null);
|
assert(widget.handleRecorded != null);
|
||||||
final RenderRepaintBoundary boundary = boundaryKey.currentContext!.findRenderObject()! as RenderRepaintBoundary;
|
final _RenderRootableRepaintBoundary boundary = boundaryKey.currentContext!.findRenderObject()! as _RenderRootableRepaintBoundary;
|
||||||
widget.handleRecorded!(boundary.toImage());
|
if (widget.allLayers) {
|
||||||
|
widget.handleRecorded!(boundary.allLayersToImage());
|
||||||
|
} else {
|
||||||
|
widget.handleRecorded!(boundary.toImage());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -239,7 +350,7 @@ class _AnimationSheetRecorderState extends State<_AnimationSheetRecorder> {
|
|||||||
alignment: Alignment.topLeft,
|
alignment: Alignment.topLeft,
|
||||||
child: SizedBox.fromSize(
|
child: SizedBox.fromSize(
|
||||||
size: widget.size,
|
size: widget.size,
|
||||||
child: RepaintBoundary(
|
child: _RootableRepaintBoundary(
|
||||||
key: boundaryKey,
|
key: boundaryKey,
|
||||||
child: _PostFrameCallbacker(
|
child: _PostFrameCallbacker(
|
||||||
callback: widget.handleRecorded == null ? null : _record,
|
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.
|
// Layout children in a grid of fixed-sized cells.
|
||||||
//
|
//
|
||||||
// The sheet fills up as much space as the parent allows. The cells are
|
// 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).
|
/// 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 {
|
|||||||
/// 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 {
|
|||||||
);
|
);
|
||||||
_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
|
|||||||
// 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
|
|||||||
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
|
|||||||
@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);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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, 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();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user