Dispose OverlayEntry in TooltipState. (#117291)
diff --git a/packages/flutter/lib/src/material/tooltip.dart b/packages/flutter/lib/src/material/tooltip.dart
index 12f80d6..fd0eda9 100644
--- a/packages/flutter/lib/src/material/tooltip.dart
+++ b/packages/flutter/lib/src/material/tooltip.dart
@@ -630,6 +630,7 @@
_entry?.remove();
}
_isConcealed = false;
+ _entry?.dispose();
_entry = null;
if (_mouseIsConnected) {
Tooltip._revealLastTooltip();
diff --git a/packages/flutter/test/foundation/leak_tracking.dart b/packages/flutter/test/foundation/leak_tracking.dart
new file mode 100644
index 0000000..caa2347
--- /dev/null
+++ b/packages/flutter/test/foundation/leak_tracking.dart
@@ -0,0 +1,72 @@
+// 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/foundation.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:leak_tracker/leak_tracker.dart';
+
+typedef LeaksObtainer = void Function(Leaks foundLeaks);
+
+/// Wrapper for [withLeakTracking] with Flutter specific functionality.
+///
+/// The method will fail if wrapped code contains memory leaks.
+///
+/// See details in documentation for `withLeakTracking` at
+/// https://github.com/dart-lang/leak_tracker/blob/main/lib/src/orchestration.dart#withLeakTracking
+///
+/// The Flutter related enhancements are:
+/// 1. Listens to [MemoryAllocations] events.
+/// 2. Uses `tester.runAsync` for leak detection if [tester] is provided.
+///
+/// If you use [testWidgets], pass [tester] to avoid async issues in leak processing.
+/// Pass null otherwise.
+///
+/// Pass [leaksObtainer] if you want to get leak information before
+/// the method failure.
+Future<void> withFlutterLeakTracking(
+ DartAsyncCallback callback, {
+ required WidgetTester? tester,
+ StackTraceCollectionConfig stackTraceCollectionConfig =
+ const StackTraceCollectionConfig(),
+ Duration? timeoutForFinalGarbageCollection,
+ LeaksObtainer? leaksObtainer,
+}) async {
+ // The method is copied (with improvements) from
+ // `package:leak_tracker/test/test_infra/flutter_helpers.dart`.
+
+ // The method is not combined with [testWidgets], because the combining will
+ // impact VSCode's ability to recognize tests.
+
+ // Leak tracker does not work for web platform.
+ if (kIsWeb) {
+ await callback();
+ return;
+ }
+
+ void flutterEventToLeakTracker(ObjectEvent event) {
+ return dispatchObjectEvent(event.toMap());
+ }
+
+ return TestAsyncUtils.guard<void>(() async {
+ MemoryAllocations.instance.addListener(flutterEventToLeakTracker);
+ final AsyncCodeRunner asyncCodeRunner = tester == null
+ ? (DartAsyncCallback action) async => action()
+ : (DartAsyncCallback action) async => tester.runAsync(action);
+ try {
+ final Leaks leaks = await withLeakTracking(
+ callback,
+ asyncCodeRunner: asyncCodeRunner,
+ stackTraceCollectionConfig: stackTraceCollectionConfig,
+ shouldThrowOnLeaks: false,
+ timeoutForFinalGarbageCollection: timeoutForFinalGarbageCollection,
+ );
+ if (leaksObtainer != null) {
+ leaksObtainer(leaks);
+ }
+ expect(leaks, isLeakFree);
+ } finally {
+ MemoryAllocations.instance.removeListener(flutterEventToLeakTracker);
+ }
+ });
+}
diff --git a/packages/flutter/test/material/tooltip_test.dart b/packages/flutter/test/material/tooltip_test.dart
index 0af5899..6c176eb 100644
--- a/packages/flutter/test/material/tooltip_test.dart
+++ b/packages/flutter/test/material/tooltip_test.dart
@@ -4,12 +4,14 @@
import 'dart:ui';
+import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
+import '../foundation/leak_tracking.dart';
import '../rendering/mock_canvas.dart';
import '../widgets/semantics_tester.dart';
import 'feedback_tester.dart';
@@ -649,38 +651,43 @@
});
testWidgets('Custom tooltip message textAlign', (WidgetTester tester) async {
- Future<void> pumpTooltipWithTextAlign({TextAlign? textAlign}) async {
- final GlobalKey<TooltipState> tooltipKey = GlobalKey<TooltipState>();
- await tester.pumpWidget(
- MaterialApp(
- home: Tooltip(
- key: tooltipKey,
- textAlign: textAlign,
- message: tooltipText,
- child: Container(
- width: 100.0,
- height: 100.0,
- color: Colors.green[500],
+ await withFlutterLeakTracking(
+ () async {
+ Future<void> pumpTooltipWithTextAlign({TextAlign? textAlign}) async {
+ final GlobalKey<TooltipState> tooltipKey = GlobalKey<TooltipState>();
+ await tester.pumpWidget(
+ MaterialApp(
+ home: Tooltip(
+ key: tooltipKey,
+ textAlign: textAlign,
+ message: tooltipText,
+ child: Container(
+ width: 100.0,
+ height: 100.0,
+ color: Colors.green[500],
+ ),
+ ),
),
- ),
- ),
- );
- tooltipKey.currentState?.ensureTooltipVisible();
- await tester.pump(const Duration(seconds: 2)); // faded in, show timer started (and at 0.0)
- }
+ );
+ tooltipKey.currentState?.ensureTooltipVisible();
+ await tester.pump(const Duration(seconds: 2)); // faded in, show timer started (and at 0.0)
+ }
- // Default value should be TextAlign.start
- await pumpTooltipWithTextAlign();
- TextAlign textAlign = tester.widget<Text>(find.text(tooltipText)).textAlign!;
- expect(textAlign, TextAlign.start);
+ // Default value should be TextAlign.start
+ await pumpTooltipWithTextAlign();
+ TextAlign textAlign = tester.widget<Text>(find.text(tooltipText)).textAlign!;
+ expect(textAlign, TextAlign.start);
- await pumpTooltipWithTextAlign(textAlign: TextAlign.center);
- textAlign = tester.widget<Text>(find.text(tooltipText)).textAlign!;
- expect(textAlign, TextAlign.center);
+ await pumpTooltipWithTextAlign(textAlign: TextAlign.center);
+ textAlign = tester.widget<Text>(find.text(tooltipText)).textAlign!;
+ expect(textAlign, TextAlign.center);
- await pumpTooltipWithTextAlign(textAlign: TextAlign.end);
- textAlign = tester.widget<Text>(find.text(tooltipText)).textAlign!;
- expect(textAlign, TextAlign.end);
+ await pumpTooltipWithTextAlign(textAlign: TextAlign.end);
+ textAlign = tester.widget<Text>(find.text(tooltipText)).textAlign!;
+ expect(textAlign, TextAlign.end);
+ },
+ tester: tester,
+ );
});
testWidgets('Tooltip overlay respects ambient Directionality', (WidgetTester tester) async {
@@ -922,7 +929,7 @@
final Finder tooltip = find.byType(Tooltip);
expect(find.text(tooltipText), findsNothing);
- await testGestureTap(tester, tooltip);
+ await _testGestureTap(tester, tooltip);
expect(find.text(tooltipText), findsOneWidget);
// Tooltip is dismissed after showDuration expired
@@ -1697,7 +1704,7 @@
expect(semanticEvents, unorderedEquals(<dynamic>[
<String, dynamic>{
'type': 'longPress',
- 'nodeId': findDebugSemantics(object).id,
+ 'nodeId': _findDebugSemantics(object).id,
'data': <String, dynamic>{},
},
<String, dynamic>{
@@ -1790,7 +1797,7 @@
final Finder tooltip = find.byType(Tooltip);
expect(find.text(tooltipText), findsNothing);
- await testGestureTap(tester, tooltip);
+ await _testGestureTap(tester, tooltip);
expect(find.text(tooltipText), findsOneWidget);
});
@@ -1800,10 +1807,10 @@
final Finder tooltip = find.byType(Tooltip);
expect(find.text(tooltipText), findsNothing);
- await testGestureTap(tester, tooltip);
+ await _testGestureTap(tester, tooltip);
expect(find.text(tooltipText), findsNothing);
- await testGestureLongPress(tester, tooltip);
+ await _testGestureLongPress(tester, tooltip);
expect(find.text(tooltipText), findsOneWidget);
});
@@ -1813,7 +1820,7 @@
final Finder tooltip = find.byType(Tooltip);
expect(find.text(tooltipText), findsNothing);
- await testGestureTap(tester, tooltip);
+ await _testGestureTap(tester, tooltip);
expect(find.text(tooltipText), findsNothing);
});
@@ -1823,10 +1830,10 @@
final Finder tooltip = find.byType(Tooltip);
expect(find.text(tooltipText), findsNothing);
- await testGestureTap(tester, tooltip);
+ await _testGestureTap(tester, tooltip);
expect(find.text(tooltipText), findsNothing);
- await testGestureLongPress(tester, tooltip);
+ await _testGestureLongPress(tester, tooltip);
expect(find.text(tooltipText), findsNothing);
});
@@ -1836,13 +1843,13 @@
await setWidgetForTooltipMode(tester, TooltipTriggerMode.longPress, onTriggered: onTriggered);
Finder tooltip = find.byType(Tooltip);
- await testGestureLongPress(tester, tooltip);
+ await _testGestureLongPress(tester, tooltip);
expect(onTriggeredCalled, true);
onTriggeredCalled = false;
await setWidgetForTooltipMode(tester, TooltipTriggerMode.tap, onTriggered: onTriggered);
tooltip = find.byType(Tooltip);
- await testGestureTap(tester, tooltip);
+ await _testGestureTap(tester, tooltip);
expect(onTriggeredCalled, true);
});
@@ -1925,7 +1932,7 @@
);
}
-Future<void> testGestureLongPress(WidgetTester tester, Finder tooltip) async {
+Future<void> _testGestureLongPress(WidgetTester tester, Finder tooltip) async {
final TestGesture gestureLongPress = await tester.startGesture(tester.getCenter(tooltip));
await tester.pump();
await tester.pump(kLongPressTimeout);
@@ -1933,14 +1940,14 @@
await tester.pump();
}
-Future<void> testGestureTap(WidgetTester tester, Finder tooltip) async {
+Future<void> _testGestureTap(WidgetTester tester, Finder tooltip) async {
await tester.tap(tooltip);
await tester.pump(const Duration(milliseconds: 10));
}
-SemanticsNode findDebugSemantics(RenderObject object) {
+SemanticsNode _findDebugSemantics(RenderObject object) {
if (object.debugSemantics != null) {
return object.debugSemantics!;
}
- return findDebugSemantics(object.parent! as RenderObject);
+ return _findDebugSemantics(object.parent! as RenderObject);
}