Fix leaks (#126144)
Fixes https://github.com/flutter/flutter/issues/126096. Fixes https://github.com/flutter/flutter/issues/126097. Fixes https://github.com/flutter/flutter/issues/126102. Fixes https://github.com/flutter/flutter/issues/126098. Fixes https://github.com/flutter/flutter/issues/126147. Work towards https://github.com/flutter/flutter/issues/126100. Does not fix the OverlyEntry/ModalRoute leak (https://github.com/flutter/flutter/issues/126100).
This commit is contained in:
parent
65dfb555c0
commit
9b230d239a
@ -297,7 +297,7 @@ class _ZoomEnterTransition extends StatefulWidget {
|
|||||||
State<_ZoomEnterTransition> createState() => _ZoomEnterTransitionState();
|
State<_ZoomEnterTransition> createState() => _ZoomEnterTransitionState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ZoomEnterTransitionState extends State<_ZoomEnterTransition> with _ZoomTransitionBase {
|
class _ZoomEnterTransitionState extends State<_ZoomEnterTransition> with _ZoomTransitionBase<_ZoomEnterTransition> {
|
||||||
// See SnapshotWidget doc comment, this is disabled on web because the HTML backend doesn't
|
// See SnapshotWidget doc comment, this is disabled on web because the HTML backend doesn't
|
||||||
// support this functionality and the canvaskit backend uses a single thread for UI and raster
|
// support this functionality and the canvaskit backend uses a single thread for UI and raster
|
||||||
// work which diminishes the impact of this performance improvement.
|
// work which diminishes the impact of this performance improvement.
|
||||||
@ -406,7 +406,7 @@ class _ZoomExitTransition extends StatefulWidget {
|
|||||||
State<_ZoomExitTransition> createState() => _ZoomExitTransitionState();
|
State<_ZoomExitTransition> createState() => _ZoomExitTransitionState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ZoomExitTransitionState extends State<_ZoomExitTransition> with _ZoomTransitionBase {
|
class _ZoomExitTransitionState extends State<_ZoomExitTransition> with _ZoomTransitionBase<_ZoomExitTransition> {
|
||||||
late _ZoomExitTransitionPainter delegate;
|
late _ZoomExitTransitionPainter delegate;
|
||||||
|
|
||||||
// See SnapshotWidget doc comment, this is disabled on web because the HTML backend doesn't
|
// See SnapshotWidget doc comment, this is disabled on web because the HTML backend doesn't
|
||||||
@ -830,7 +830,7 @@ void _updateScaledTransform(Matrix4 transform, double scale, Size size) {
|
|||||||
transform.translate(-dx, -dy);
|
transform.translate(-dx, -dy);
|
||||||
}
|
}
|
||||||
|
|
||||||
mixin _ZoomTransitionBase {
|
mixin _ZoomTransitionBase<S extends StatefulWidget> on State<S> {
|
||||||
bool get useSnapshot;
|
bool get useSnapshot;
|
||||||
|
|
||||||
// Don't rasterize if:
|
// Don't rasterize if:
|
||||||
@ -863,6 +863,12 @@ mixin _ZoomTransitionBase {
|
|||||||
controller.allowSnapshotting = useSnapshot;
|
controller.allowSnapshotting = useSnapshot;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
controller.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ZoomEnterTransitionPainter extends SnapshotPainter {
|
class _ZoomEnterTransitionPainter extends SnapshotPainter {
|
||||||
|
@ -885,6 +885,7 @@ class _DragAvatar<T extends Object> extends Drag {
|
|||||||
_leaveAllEntered();
|
_leaveAllEntered();
|
||||||
_activeTarget = null;
|
_activeTarget = null;
|
||||||
_entry!.remove();
|
_entry!.remove();
|
||||||
|
_entry!.dispose();
|
||||||
_entry = null;
|
_entry = null;
|
||||||
// TODO(ianh): consider passing _entry as well so the client can perform an animation.
|
// TODO(ianh): consider passing _entry as well so the client can perform an animation.
|
||||||
onDragEnd?.call(velocity ?? Velocity.zero, _lastOffset!, wasAccepted);
|
onDragEnd?.call(velocity ?? Velocity.zero, _lastOffset!, wasAccepted);
|
||||||
|
@ -454,6 +454,7 @@ abstract class Route<T> {
|
|||||||
@protected
|
@protected
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_navigator = null;
|
_navigator = null;
|
||||||
|
_restorationScopeId.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether this route is the top-most route on the navigator.
|
/// Whether this route is the top-most route on the navigator.
|
||||||
@ -3606,6 +3607,9 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin, Res
|
|||||||
for (final _RouteEntry entry in _history) {
|
for (final _RouteEntry entry in _history) {
|
||||||
entry.dispose();
|
entry.dispose();
|
||||||
}
|
}
|
||||||
|
_rawNextPagelessRestorationScopeId.dispose();
|
||||||
|
_serializableHistory.dispose();
|
||||||
|
userGestureInProgressNotifier.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
// don't unlock, so that the object becomes unusable
|
// don't unlock, so that the object becomes unusable
|
||||||
assert(_debugLocked);
|
assert(_debugLocked);
|
||||||
|
@ -64,13 +64,13 @@ class TickerMode extends StatefulWidget {
|
|||||||
return widget?.enabled ?? true;
|
return widget?.enabled ?? true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Obtains a [ValueNotifier] from the [TickerMode] surrounding the `context`,
|
/// Obtains a [ValueListenable] from the [TickerMode] surrounding the `context`,
|
||||||
/// which indicates whether tickers are enabled in the given subtree.
|
/// which indicates whether tickers are enabled in the given subtree.
|
||||||
///
|
///
|
||||||
/// When that [TickerMode] enabled or disabled tickers, the notifier notifies
|
/// When that [TickerMode] enabled or disabled tickers, the listenable notifies
|
||||||
/// its listeners.
|
/// its listeners.
|
||||||
///
|
///
|
||||||
/// While the [ValueNotifier] is stable for the lifetime of the surrounding
|
/// While the [ValueListenable] is stable for the lifetime of the surrounding
|
||||||
/// [TickerMode], calling this method does not establish a dependency between
|
/// [TickerMode], calling this method does not establish a dependency between
|
||||||
/// the `context` and the [TickerMode] and the widget owning the `context`
|
/// the `context` and the [TickerMode] and the widget owning the `context`
|
||||||
/// does not rebuild when the ticker mode changes from true to false or vice
|
/// does not rebuild when the ticker mode changes from true to false or vice
|
||||||
@ -79,7 +79,7 @@ class TickerMode extends StatefulWidget {
|
|||||||
/// [Ticker]. Since no dependency is established, the widget owning the
|
/// [Ticker]. Since no dependency is established, the widget owning the
|
||||||
/// `context` is also not informed when it is moved to a new location in the
|
/// `context` is also not informed when it is moved to a new location in the
|
||||||
/// tree where it may have a different [TickerMode] ancestor. When this
|
/// tree where it may have a different [TickerMode] ancestor. When this
|
||||||
/// happens, the widget must manually unsubscribe from the old notifier,
|
/// happens, the widget must manually unsubscribe from the old listenable,
|
||||||
/// obtain a new one from the new ancestor [TickerMode] by calling this method
|
/// obtain a new one from the new ancestor [TickerMode] by calling this method
|
||||||
/// again, and re-subscribe to it. [StatefulWidget]s can, for example, do this
|
/// again, and re-subscribe to it. [StatefulWidget]s can, for example, do this
|
||||||
/// in [State.activate], which is called after the widget has been moved to
|
/// in [State.activate], which is called after the widget has been moved to
|
||||||
@ -93,10 +93,10 @@ class TickerMode extends StatefulWidget {
|
|||||||
/// potential unnecessary rebuilds.
|
/// potential unnecessary rebuilds.
|
||||||
///
|
///
|
||||||
/// In the absence of a [TickerMode] widget, this function returns a
|
/// In the absence of a [TickerMode] widget, this function returns a
|
||||||
/// [ValueNotifier], whose [ValueNotifier.value] is always true.
|
/// [ValueListenable], whose [ValueListenable.value] is always true.
|
||||||
static ValueNotifier<bool> getNotifier(BuildContext context) {
|
static ValueListenable<bool> getNotifier(BuildContext context) {
|
||||||
final _EffectiveTickerMode? widget = context.getInheritedWidgetOfExactType<_EffectiveTickerMode>();
|
final _EffectiveTickerMode? widget = context.getInheritedWidgetOfExactType<_EffectiveTickerMode>();
|
||||||
return widget?.notifier ?? ValueNotifier<bool>(true);
|
return widget?.notifier ?? const _ConstantValueListenable<bool>(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -228,7 +228,7 @@ mixin SingleTickerProviderStateMixin<T extends StatefulWidget> on State<T> imple
|
|||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
ValueNotifier<bool>? _tickerModeNotifier;
|
ValueListenable<bool>? _tickerModeNotifier;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void activate() {
|
void activate() {
|
||||||
@ -245,7 +245,7 @@ mixin SingleTickerProviderStateMixin<T extends StatefulWidget> on State<T> imple
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _updateTickerModeNotifier() {
|
void _updateTickerModeNotifier() {
|
||||||
final ValueNotifier<bool> newNotifier = TickerMode.getNotifier(context);
|
final ValueListenable<bool> newNotifier = TickerMode.getNotifier(context);
|
||||||
if (newNotifier == _tickerModeNotifier) {
|
if (newNotifier == _tickerModeNotifier) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -307,7 +307,7 @@ mixin TickerProviderStateMixin<T extends StatefulWidget> on State<T> implements
|
|||||||
_tickers!.remove(ticker);
|
_tickers!.remove(ticker);
|
||||||
}
|
}
|
||||||
|
|
||||||
ValueNotifier<bool>? _tickerModeNotifier;
|
ValueListenable<bool>? _tickerModeNotifier;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void activate() {
|
void activate() {
|
||||||
@ -327,7 +327,7 @@ mixin TickerProviderStateMixin<T extends StatefulWidget> on State<T> implements
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _updateTickerModeNotifier() {
|
void _updateTickerModeNotifier() {
|
||||||
final ValueNotifier<bool> newNotifier = TickerMode.getNotifier(context);
|
final ValueListenable<bool> newNotifier = TickerMode.getNotifier(context);
|
||||||
if (newNotifier == _tickerModeNotifier) {
|
if (newNotifier == _tickerModeNotifier) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -395,3 +395,22 @@ class _WidgetTicker extends Ticker {
|
|||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _ConstantValueListenable<T> implements ValueListenable<T> {
|
||||||
|
const _ConstantValueListenable(this.value);
|
||||||
|
|
||||||
|
@override
|
||||||
|
void addListener(VoidCallback listener) {
|
||||||
|
// Intentionally left empty: Value cannot change, so we never have to
|
||||||
|
// notify registered listeners.
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void removeListener(VoidCallback listener) {
|
||||||
|
// Intentionally left empty: Value cannot change, so we never have to
|
||||||
|
// notify registered listeners.
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
final T value;
|
||||||
|
}
|
||||||
|
@ -7,18 +7,20 @@
|
|||||||
@Tags(<String>['reduced-test-set'])
|
@Tags(<String>['reduced-test-set'])
|
||||||
library;
|
library;
|
||||||
|
|
||||||
|
import 'dart:ui' as ui;
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
import '../foundation/leak_tracking.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
/*
|
/*
|
||||||
* Here lies tests for packages/flutter_test/lib/src/animation_sheet.dart
|
* Here lies tests for packages/flutter_test/lib/src/animation_sheet.dart
|
||||||
* because [matchesGoldenFile] does not use Skia Gold in its native package.
|
* because [matchesGoldenFile] does not use Skia Gold in its native package.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// TODO(polina-c): fix ValueNotifier not disposed and switch to testWidgetsWithLeakTracking.
|
testWidgetsWithLeakTracking('correctly records frames using display', (WidgetTester tester) async {
|
||||||
// https://github.com/flutter/devtools/issues/3951
|
|
||||||
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(
|
||||||
@ -54,9 +56,7 @@ void main() {
|
|||||||
await expectLater(find.byWidget(display), matchesGoldenFile('test.animation_sheet_builder.records.png'));
|
await expectLater(find.byWidget(display), matchesGoldenFile('test.animation_sheet_builder.records.png'));
|
||||||
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/56001
|
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/56001
|
||||||
|
|
||||||
// TODO(polina-c): fix ValueNotifier not disposed and switch to testWidgetsWithLeakTracking.
|
testWidgetsWithLeakTracking('correctly wraps a row', (WidgetTester tester) async {
|
||||||
// https://github.com/flutter/devtools/issues/3951
|
|
||||||
testWidgets('correctly wraps a row', (WidgetTester tester) async {
|
|
||||||
final AnimationSheetBuilder builder = AnimationSheetBuilder(frameSize: _DecuplePixels.size);
|
final AnimationSheetBuilder builder = AnimationSheetBuilder(frameSize: _DecuplePixels.size);
|
||||||
|
|
||||||
const Duration duration = Duration(seconds: 2);
|
const Duration duration = Duration(seconds: 2);
|
||||||
@ -74,9 +74,7 @@ void main() {
|
|||||||
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/56001
|
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/56001
|
||||||
|
|
||||||
// TODO(polina-c): fix Picture and Image not disposed and and switch to testWidgetsWithLeakTracking.
|
testWidgetsWithLeakTracking('correctly records frames using collate', (WidgetTester tester) async {
|
||||||
// https://github.com/flutter/devtools/issues/3951
|
|
||||||
testWidgets('correctly records frames using collate', (WidgetTester tester) async {
|
|
||||||
final AnimationSheetBuilder builder = AnimationSheetBuilder(frameSize: _DecuplePixels.size);
|
final AnimationSheetBuilder builder = AnimationSheetBuilder(frameSize: _DecuplePixels.size);
|
||||||
|
|
||||||
await tester.pumpFrames(
|
await tester.pumpFrames(
|
||||||
@ -104,15 +102,16 @@ void main() {
|
|||||||
const Duration(milliseconds: 100),
|
const Duration(milliseconds: 100),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
final ui.Image image = await builder.collate(5);
|
||||||
|
|
||||||
await expectLater(
|
await expectLater(
|
||||||
builder.collate(5),
|
image,
|
||||||
matchesGoldenFile('test.animation_sheet_builder.collate.png'),
|
matchesGoldenFile('test.animation_sheet_builder.collate.png'),
|
||||||
);
|
);
|
||||||
|
image.dispose();
|
||||||
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/56001
|
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/56001
|
||||||
|
|
||||||
// TODO(polina-c): fix Picture and Image not disposed and switch to testWidgetsWithLeakTracking.
|
testWidgetsWithLeakTracking('use allLayers to record out-of-subtree contents', (WidgetTester tester) async {
|
||||||
// https://github.com/flutter/devtools/issues/3951
|
|
||||||
testWidgets('use allLayers to record out-of-subtree contents', (WidgetTester tester) async {
|
|
||||||
final AnimationSheetBuilder builder = AnimationSheetBuilder(
|
final AnimationSheetBuilder builder = AnimationSheetBuilder(
|
||||||
frameSize: const Size(8, 2),
|
frameSize: const Size(8, 2),
|
||||||
allLayers: true,
|
allLayers: true,
|
||||||
@ -137,12 +136,14 @@ void main() {
|
|||||||
const Duration(milliseconds: 100),
|
const Duration(milliseconds: 100),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
final ui.Image image = await builder.collate(5);
|
||||||
|
|
||||||
await expectLater(
|
await expectLater(
|
||||||
builder.collate(5),
|
image,
|
||||||
matchesGoldenFile('test.animation_sheet_builder.out_of_tree.png'),
|
matchesGoldenFile('test.animation_sheet_builder.out_of_tree.png'),
|
||||||
);
|
);
|
||||||
|
image.dispose();
|
||||||
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/56001
|
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/56001
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
||||||
|
@ -7,6 +7,8 @@
|
|||||||
@Tags(<String>['reduced-test-set'])
|
@Tags(<String>['reduced-test-set'])
|
||||||
library;
|
library;
|
||||||
|
|
||||||
|
import 'dart:ui' as ui;
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
@ -78,9 +80,7 @@ void main() {
|
|||||||
// Currently skipped due to daily flake: https://github.com/flutter/flutter/issues/87588
|
// Currently skipped due to daily flake: https://github.com/flutter/flutter/issues/87588
|
||||||
}, skip: true); // Typically skip: isBrowser https://github.com/flutter/flutter/issues/42767
|
}, skip: true); // Typically skip: isBrowser https://github.com/flutter/flutter/issues/42767
|
||||||
|
|
||||||
// TODO(polina-c): fix ValueNotifier not disposed and switch to testWidgetsWithLeakTracking.
|
testWidgetsWithLeakTracking('Should show event indicator for pointer events with setSurfaceSize', (WidgetTester tester) async {
|
||||||
// https://github.com/flutter/devtools/issues/3951
|
|
||||||
testWidgets('Should show event indicator for pointer events with setSurfaceSize', (WidgetTester tester) async {
|
|
||||||
final AnimationSheetBuilder animationSheet = AnimationSheetBuilder(frameSize: const Size(200, 200), allLayers: true);
|
final AnimationSheetBuilder animationSheet = AnimationSheetBuilder(frameSize: const Size(200, 200), allLayers: true);
|
||||||
final List<Offset> taps = <Offset>[];
|
final List<Offset> taps = <Offset>[];
|
||||||
Widget target({bool recording = true}) => Container(
|
Widget target({bool recording = true}) => Container(
|
||||||
@ -132,9 +132,12 @@ void main() {
|
|||||||
await tester.pumpFrames(target(), const Duration(milliseconds: 50));
|
await tester.pumpFrames(target(), const Duration(milliseconds: 50));
|
||||||
expect(taps, isEmpty);
|
expect(taps, isEmpty);
|
||||||
|
|
||||||
|
final ui.Image image = await animationSheet.collate(6);
|
||||||
|
|
||||||
await expectLater(
|
await expectLater(
|
||||||
animationSheet.collate(6),
|
image,
|
||||||
matchesGoldenFile('LiveBinding.press.animation.2.png'),
|
matchesGoldenFile('LiveBinding.press.animation.2.png'),
|
||||||
);
|
);
|
||||||
|
image.dispose();
|
||||||
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/56001
|
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/56001
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,8 @@ import 'package:flutter_test/flutter_test.dart';
|
|||||||
import 'package:leak_tracker/leak_tracker.dart';
|
import 'package:leak_tracker/leak_tracker.dart';
|
||||||
import 'package:meta/meta.dart';
|
import 'package:meta/meta.dart';
|
||||||
|
|
||||||
|
export 'package:leak_tracker/leak_tracker.dart' show LeakTrackingTestConfig, StackTraceCollectionConfig;
|
||||||
|
|
||||||
/// Set of objects, that does not hold the objects from garbage collection.
|
/// Set of objects, that does not hold the objects from garbage collection.
|
||||||
///
|
///
|
||||||
/// The objects are referenced by hash codes and can duplicate with low probability.
|
/// The objects are referenced by hash codes and can duplicate with low probability.
|
||||||
|
@ -114,9 +114,7 @@ void main() {
|
|||||||
expect(result.dragUpdate, true);
|
expect(result.dragUpdate, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO(polina-c): fix ValueNotifier not disposed and switch to testWidgetsWithLeakTracking.
|
testWidgetsWithLeakTracking('Scroll Views get the same ScrollConfiguration as Draggables', (WidgetTester tester) async {
|
||||||
// https://github.com/flutter/devtools/issues/3951
|
|
||||||
testWidgets('Scroll Views get the same ScrollConfiguration as Draggables', (WidgetTester tester) async {
|
|
||||||
tester.view.gestureSettings = const ui.GestureSettings(physicalTouchSlop: 4);
|
tester.view.gestureSettings = const ui.GestureSettings(physicalTouchSlop: 4);
|
||||||
addTearDown(tester.view.reset);
|
addTearDown(tester.view.reset);
|
||||||
|
|
||||||
|
@ -150,9 +150,7 @@ void main() {
|
|||||||
expect(find.text('About flutter_tester'), findsOneWidget);
|
expect(find.text('About flutter_tester'), findsOneWidget);
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO(polina-c): fix SnapshotController not disposed and switch to testWidgetsWithLeakTracking.
|
testWidgetsWithLeakTracking('LicensePage control test', (WidgetTester tester) async {
|
||||||
// https://github.com/flutter/devtools/issues/3951
|
|
||||||
testWidgets('LicensePage control test', (WidgetTester tester) async {
|
|
||||||
LicenseRegistry.addLicense(() {
|
LicenseRegistry.addLicense(() {
|
||||||
return Stream<LicenseEntry>.fromIterable(<LicenseEntry>[
|
return Stream<LicenseEntry>.fromIterable(<LicenseEntry>[
|
||||||
const LicenseEntryWithLineBreaks(<String>['AAA'], 'BBB'),
|
const LicenseEntryWithLineBreaks(<String>['AAA'], 'BBB'),
|
||||||
@ -203,9 +201,7 @@ void main() {
|
|||||||
expect(find.text('Another license'), findsOneWidget);
|
expect(find.text('Another license'), findsOneWidget);
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO(polina-c): fix SnapshotController not disposed and switch to testWidgetsWithLeakTracking.
|
testWidgetsWithLeakTracking('LicensePage control test with all properties', (WidgetTester tester) async {
|
||||||
// https://github.com/flutter/devtools/issues/3951
|
|
||||||
testWidgets('LicensePage control test with all properties', (WidgetTester tester) async {
|
|
||||||
const FlutterLogo logo = FlutterLogo();
|
const FlutterLogo logo = FlutterLogo();
|
||||||
|
|
||||||
LicenseRegistry.addLicense(() {
|
LicenseRegistry.addLicense(() {
|
||||||
@ -408,9 +404,7 @@ void main() {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO(polina-c): fix SnapshotController not disposed and switch to testWidgetsWithLeakTracking.
|
testWidgetsWithLeakTracking('LicensePage returns early if unmounted', (WidgetTester tester) async {
|
||||||
// https://github.com/flutter/devtools/issues/3951
|
|
||||||
testWidgets('LicensePage returns early if unmounted', (WidgetTester tester) async {
|
|
||||||
final Completer<LicenseEntry> licenseCompleter = Completer<LicenseEntry>();
|
final Completer<LicenseEntry> licenseCompleter = Completer<LicenseEntry>();
|
||||||
LicenseRegistry.addLicense(() {
|
LicenseRegistry.addLicense(() {
|
||||||
return Stream<LicenseEntry>.fromFuture(licenseCompleter.future);
|
return Stream<LicenseEntry>.fromFuture(licenseCompleter.future);
|
||||||
@ -433,11 +427,9 @@ void main() {
|
|||||||
final FakeLicenseEntry licenseEntry = FakeLicenseEntry();
|
final FakeLicenseEntry licenseEntry = FakeLicenseEntry();
|
||||||
licenseCompleter.complete(licenseEntry);
|
licenseCompleter.complete(licenseEntry);
|
||||||
expect(licenseEntry.packagesCalled, false);
|
expect(licenseEntry.packagesCalled, false);
|
||||||
});
|
}, leakTrackingConfig: const LeakTrackingTestConfig(notDisposedAllowList: <String>{'ValueNotifier<_OverlayEntryWidgetState?>'})); // TODO(goderbauer): Fix leak, https://github.com/flutter/flutter/issues/126100.
|
||||||
|
|
||||||
// TODO(polina-c): fix SnapshotController not disposed and switch to testWidgetsWithLeakTracking.
|
testWidgetsWithLeakTracking('LicensePage returns late if unmounted', (WidgetTester tester) async {
|
||||||
// https://github.com/flutter/devtools/issues/3951
|
|
||||||
testWidgets('LicensePage returns late if unmounted', (WidgetTester tester) async {
|
|
||||||
final Completer<LicenseEntry> licenseCompleter = Completer<LicenseEntry>();
|
final Completer<LicenseEntry> licenseCompleter = Completer<LicenseEntry>();
|
||||||
LicenseRegistry.addLicense(() {
|
LicenseRegistry.addLicense(() {
|
||||||
return Stream<LicenseEntry>.fromFuture(licenseCompleter.future);
|
return Stream<LicenseEntry>.fromFuture(licenseCompleter.future);
|
||||||
@ -460,7 +452,7 @@ void main() {
|
|||||||
|
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
expect(licenseEntry.packagesCalled, true);
|
expect(licenseEntry.packagesCalled, true);
|
||||||
});
|
}, leakTrackingConfig: const LeakTrackingTestConfig(notDisposedAllowList: <String>{'ValueNotifier<_OverlayEntryWidgetState?>'})); // TODO(goderbauer): Fix leak, https://github.com/flutter/flutter/issues/126100.
|
||||||
|
|
||||||
testWidgetsWithLeakTracking('LicensePage logic defaults to executable name for app name', (WidgetTester tester) async {
|
testWidgetsWithLeakTracking('LicensePage logic defaults to executable name for app name', (WidgetTester tester) async {
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
@ -1075,9 +1067,7 @@ void main() {
|
|||||||
expect(find.text('Exception: Injected failure'), findsOneWidget);
|
expect(find.text('Exception: Injected failure'), findsOneWidget);
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO(polina-c): fix SnapshotController not disposed and switch to testWidgetsWithLeakTracking.
|
testWidgetsWithLeakTracking('LicensePage master view layout position - ltr', (WidgetTester tester) async {
|
||||||
// https://github.com/flutter/devtools/issues/3951
|
|
||||||
testWidgets('LicensePage master view layout position - ltr', (WidgetTester tester) async {
|
|
||||||
const TextDirection textDirection = TextDirection.ltr;
|
const TextDirection textDirection = TextDirection.ltr;
|
||||||
const Size defaultSize = Size(800.0, 600.0);
|
const Size defaultSize = Size(800.0, 600.0);
|
||||||
const Size wideSize = Size(1200.0, 600.0);
|
const Size wideSize = Size(1200.0, 600.0);
|
||||||
@ -1138,11 +1128,9 @@ void main() {
|
|||||||
|
|
||||||
// Configure to show the default layout.
|
// Configure to show the default layout.
|
||||||
await tester.binding.setSurfaceSize(defaultSize);
|
await tester.binding.setSurfaceSize(defaultSize);
|
||||||
});
|
}, leakTrackingConfig: const LeakTrackingTestConfig(notDisposedAllowList: <String>{'ValueNotifier<_OverlayEntryWidgetState?>'})); // TODO(goderbauer): Fix leak, https://github.com/flutter/flutter/issues/126100.
|
||||||
|
|
||||||
// TODO(polina-c): fix SnapshotController not disposed and switch to testWidgetsWithLeakTracking.
|
testWidgetsWithLeakTracking('LicensePage master view layout position - rtl', (WidgetTester tester) async {
|
||||||
// https://github.com/flutter/devtools/issues/3951
|
|
||||||
testWidgets('LicensePage master view layout position - rtl', (WidgetTester tester) async {
|
|
||||||
const TextDirection textDirection = TextDirection.rtl;
|
const TextDirection textDirection = TextDirection.rtl;
|
||||||
const Size defaultSize = Size(800.0, 600.0);
|
const Size defaultSize = Size(800.0, 600.0);
|
||||||
const Size wideSize = Size(1200.0, 600.0);
|
const Size wideSize = Size(1200.0, 600.0);
|
||||||
@ -1203,7 +1191,7 @@ void main() {
|
|||||||
|
|
||||||
// Configure to show the default layout.
|
// Configure to show the default layout.
|
||||||
await tester.binding.setSurfaceSize(defaultSize);
|
await tester.binding.setSurfaceSize(defaultSize);
|
||||||
});
|
}, leakTrackingConfig: const LeakTrackingTestConfig(notDisposedAllowList: <String>{'ValueNotifier<_OverlayEntryWidgetState?>'})); // TODO(goderbauer): Fix leak, https://github.com/flutter/flutter/issues/126100.
|
||||||
|
|
||||||
testWidgetsWithLeakTracking('License page title in lateral UI does not use AppBarTheme.foregroundColor', (WidgetTester tester) async {
|
testWidgetsWithLeakTracking('License page title in lateral UI does not use AppBarTheme.foregroundColor', (WidgetTester tester) async {
|
||||||
// This is a regression test for https://github.com/flutter/flutter/issues/108991
|
// This is a regression test for https://github.com/flutter/flutter/issues/108991
|
||||||
|
@ -10,10 +10,10 @@ library;
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
import '../foundation/leak_tracking.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
// TODO(polina-c): fix Image not disposed and switch to testWidgetsWithLeakTracking.
|
testWidgetsWithLeakTracking('Flutter Logo golden test', (WidgetTester tester) async {
|
||||||
// https://github.com/flutter/devtools/issues/3951
|
|
||||||
testWidgets('Flutter Logo golden test', (WidgetTester tester) async {
|
|
||||||
final Key logo = UniqueKey();
|
final Key logo = UniqueKey();
|
||||||
await tester.pumpWidget(FlutterLogo(key: logo));
|
await tester.pumpWidget(FlutterLogo(key: logo));
|
||||||
|
|
||||||
|
@ -201,13 +201,16 @@ Future<ComparisonResult> compareLists(List<int>? test, List<int>? master) async
|
|||||||
final int height = testImage.height;
|
final int height = testImage.height;
|
||||||
|
|
||||||
if (width != masterImage.width || height != masterImage.height) {
|
if (width != masterImage.width || height != masterImage.height) {
|
||||||
return ComparisonResult(
|
final ComparisonResult result = ComparisonResult(
|
||||||
passed: false,
|
passed: false,
|
||||||
diffPercent: 1.0,
|
diffPercent: 1.0,
|
||||||
error: 'Pixel test failed, image sizes do not match.\n'
|
error: 'Pixel test failed, image sizes do not match.\n'
|
||||||
'Master Image: ${masterImage.width} X ${masterImage.height}\n'
|
'Master Image: ${masterImage.width} X ${masterImage.height}\n'
|
||||||
'Test Image: ${testImage.width} X ${testImage.height}',
|
'Test Image: ${testImage.width} X ${testImage.height}',
|
||||||
);
|
);
|
||||||
|
masterImage.dispose();
|
||||||
|
testImage.dispose();
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
int pixelDiffCount = 0;
|
int pixelDiffCount = 0;
|
||||||
@ -264,6 +267,8 @@ Future<ComparisonResult> compareLists(List<int>? test, List<int>? master) async
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
masterImage.dispose();
|
||||||
|
testImage.dispose();
|
||||||
return ComparisonResult(passed: true, diffPercent: 0.0);
|
return ComparisonResult(passed: true, diffPercent: 0.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -78,10 +78,13 @@ class MatchesGoldenFile extends AsyncMatcher {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Future<ui.Image?> imageFuture;
|
Future<ui.Image?> imageFuture;
|
||||||
|
final bool disposeImage; // set to true if the matcher created and owns the image and must therefore dispose it.
|
||||||
if (item is Future<ui.Image?>) {
|
if (item is Future<ui.Image?>) {
|
||||||
imageFuture = item;
|
imageFuture = item;
|
||||||
|
disposeImage = false;
|
||||||
} else if (item is ui.Image) {
|
} else if (item is ui.Image) {
|
||||||
imageFuture = Future<ui.Image>.value(item);
|
imageFuture = Future<ui.Image>.value(item);
|
||||||
|
disposeImage = false;
|
||||||
} else if (item is Finder) {
|
} else if (item is Finder) {
|
||||||
final Iterable<Element> elements = item.evaluate();
|
final Iterable<Element> elements = item.evaluate();
|
||||||
if (elements.isEmpty) {
|
if (elements.isEmpty) {
|
||||||
@ -90,6 +93,7 @@ class MatchesGoldenFile extends AsyncMatcher {
|
|||||||
return 'matched too many widgets';
|
return 'matched too many widgets';
|
||||||
}
|
}
|
||||||
imageFuture = captureImage(elements.single);
|
imageFuture = captureImage(elements.single);
|
||||||
|
disposeImage = true;
|
||||||
} else {
|
} else {
|
||||||
throw AssertionError('must provide a Finder, Image, Future<Image>, List<int>, or Future<List<int>>');
|
throw AssertionError('must provide a Finder, Image, Future<Image>, List<int>, or Future<List<int>>');
|
||||||
}
|
}
|
||||||
@ -100,6 +104,7 @@ class MatchesGoldenFile extends AsyncMatcher {
|
|||||||
if (image == null) {
|
if (image == null) {
|
||||||
throw AssertionError('Future<Image> completed to null');
|
throw AssertionError('Future<Image> completed to null');
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
final ByteData? bytes = await image.toByteData(format: ui.ImageByteFormat.png);
|
final ByteData? bytes = await image.toByteData(format: ui.ImageByteFormat.png);
|
||||||
if (bytes == null) {
|
if (bytes == null) {
|
||||||
return 'could not encode screenshot.';
|
return 'could not encode screenshot.';
|
||||||
@ -114,6 +119,11 @@ class MatchesGoldenFile extends AsyncMatcher {
|
|||||||
} on TestFailure catch (ex) {
|
} on TestFailure catch (ex) {
|
||||||
return ex.message;
|
return ex.message;
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
if (disposeImage) {
|
||||||
|
image.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
}, additionalTime: const Duration(minutes: 1));
|
}, additionalTime: const Duration(minutes: 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -437,10 +437,13 @@ Future<ui.Image> _collateFrames(List<ui.Image> frames, Size frameSize, int cells
|
|||||||
Paint(),
|
Paint(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return recorder.endRecording().toImage(
|
final ui.Picture picture = recorder.endRecording();
|
||||||
|
final ui.Image image = await picture.toImage(
|
||||||
(frameSize.width * cellsPerRow).toInt(),
|
(frameSize.width * cellsPerRow).toInt(),
|
||||||
(frameSize.height * rowNum).toInt(),
|
(frameSize.height * rowNum).toInt(),
|
||||||
);
|
);
|
||||||
|
picture.dispose();
|
||||||
|
return image;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Layout children in a grid of fixed-sized cells.
|
// Layout children in a grid of fixed-sized cells.
|
||||||
|
Loading…
x
Reference in New Issue
Block a user