diff --git a/packages/flutter/lib/src/semantics/binding.dart b/packages/flutter/lib/src/semantics/binding.dart
index fb10a7aba0890082740e0822e73c0c60b04d78fb..eb2a6d739a3708eefc17369358168c81d9ca7e1b 100644
--- a/packages/flutter/lib/src/semantics/binding.dart
+++ b/packages/flutter/lib/src/semantics/binding.dart
@@ -67,6 +67,11 @@ mixin SemanticsBinding on BindingBase {
     _semanticsEnabled.removeListener(listener);
   }
 
+  /// The number of clients registered to listen for semantics.
+  ///
+  /// The number is increased whenever [ensureSemantics] is called and decreased
+  /// when [SemanticsHandle.dispose] is called.
+  int get debugOutstandingSemanticsHandles => _outstandingHandles;
   int _outstandingHandles = 0;
 
   /// Creates a new [SemanticsHandle] and requests the collection of semantics
diff --git a/packages/flutter/test/cupertino/dialog_test.dart b/packages/flutter/test/cupertino/dialog_test.dart
index 1231a6e0b7bd6907ad39c9245c22ece920f6d249..7f19ba217c06faf3d6e2aa249e620358ab4251eb 100644
--- a/packages/flutter/test/cupertino/dialog_test.dart
+++ b/packages/flutter/test/cupertino/dialog_test.dart
@@ -1247,6 +1247,7 @@ void main() {
       label: 'Custom label',
       flags: <SemanticsFlag>[SemanticsFlag.namesRoute],
     )));
+    semantics.dispose();
   });
 
   testWidgets('CupertinoDialogRoute is state restorable', (WidgetTester tester) async {
diff --git a/packages/flutter/test/cupertino/route_test.dart b/packages/flutter/test/cupertino/route_test.dart
index 2f171cd7e758cc2e88b79d208bd920c42307196c..99186bc8373a37a1b028fa25236395688db8f369 100644
--- a/packages/flutter/test/cupertino/route_test.dart
+++ b/packages/flutter/test/cupertino/route_test.dart
@@ -1484,6 +1484,7 @@ void main() {
       label: 'Dismiss',
     )));
     debugDefaultTargetPlatformOverride = null;
+    semantics.dispose();
   });
 
   testWidgets('showCupertinoModalPopup allows for semantics dismiss when set', (WidgetTester tester) async {
@@ -1519,6 +1520,7 @@ void main() {
       label: 'Dismiss',
     ));
     debugDefaultTargetPlatformOverride = null;
+    semantics.dispose();
   });
 
   testWidgets('showCupertinoModalPopup passes RouteSettings to PopupRoute', (WidgetTester tester) async {
diff --git a/packages/flutter/test/material/calendar_date_picker_test.dart b/packages/flutter/test/material/calendar_date_picker_test.dart
index ac7788206ec46a7067b1f1550dd9952a7de05f70..acc4c54815927b1137d9512251e6dc03dfdfc16f 100644
--- a/packages/flutter/test/material/calendar_date_picker_test.dart
+++ b/packages/flutter/test/material/calendar_date_picker_test.dart
@@ -657,7 +657,6 @@ void main() {
     group('Semantics', () {
       testWidgets('day mode', (WidgetTester tester) async {
         final SemanticsHandle semantics = tester.ensureSemantics();
-        addTearDown(semantics.dispose);
 
         await tester.pumpWidget(calendarDatePicker());
 
@@ -837,11 +836,11 @@ void main() {
           hasTapAction: true,
           isFocusable: true,
         ));
+        semantics.dispose();
       });
 
       testWidgets('calendar year mode', (WidgetTester tester) async {
         final SemanticsHandle semantics = tester.ensureSemantics();
-        addTearDown(semantics.dispose);
 
         await tester.pumpWidget(calendarDatePicker(
           initialCalendarMode: DatePickerMode.year,
@@ -863,8 +862,8 @@ void main() {
             isButton: true,
           ));
         }
+        semantics.dispose();
       });
-
     });
   });
 
diff --git a/packages/flutter/test/material/date_picker_test.dart b/packages/flutter/test/material/date_picker_test.dart
index 648080cfad3a1e0240c41458ee7893ee5e3cf7b6..8795ba15592dbebd5bd5c95195ea0e63fb334fdb 100644
--- a/packages/flutter/test/material/date_picker_test.dart
+++ b/packages/flutter/test/material/date_picker_test.dart
@@ -814,7 +814,6 @@ void main() {
   group('Semantics', () {
     testWidgets('calendar mode', (WidgetTester tester) async {
       final SemanticsHandle semantics = tester.ensureSemantics();
-      addTearDown(semantics.dispose);
 
       await prepareDatePicker(tester, (Future<DateTime?> date) async {
         // Header
@@ -858,11 +857,11 @@ void main() {
           isFocusable: true,
         ));
       });
+      semantics.dispose();
     });
 
     testWidgets('input mode', (WidgetTester tester) async {
       final SemanticsHandle semantics = tester.ensureSemantics();
-      addTearDown(semantics.dispose);
 
       initialEntryMode = DatePickerEntryMode.input;
       await prepareDatePicker(tester, (Future<DateTime?> date) async {
@@ -901,6 +900,7 @@ void main() {
           isFocusable: true,
         ));
       });
+      semantics.dispose();
     });
   });
 
diff --git a/packages/flutter/test/material/date_range_picker_test.dart b/packages/flutter/test/material/date_range_picker_test.dart
index e434329f2fa2f96c57b6abc7dc5f5566dbbcc327..ef911fe85e2feb10f3ef123da09c8e96e5a01745 100644
--- a/packages/flutter/test/material/date_range_picker_test.dart
+++ b/packages/flutter/test/material/date_range_picker_test.dart
@@ -1086,21 +1086,21 @@ void main() {
     });
   });
 
-group('Semantics', () {
-  testWidgets('calendar mode', (WidgetTester tester) async {
-    final SemanticsHandle semantics = tester.ensureSemantics();
-    currentDate = DateTime(2016, DateTime.january, 30);
-    addTearDown(semantics.dispose);
-    await preparePicker(tester, (Future<DateTimeRange?> range) async {
-      expect(
-        tester.getSemantics(find.text('30')),
-        matchesSemantics(
-          label: '30, Saturday, January 30, 2016, Today',
-          hasTapAction: true,
-          isFocusable: true,
-        ),
-      );
-    });
+  group('Semantics', () {
+    testWidgets('calendar mode', (WidgetTester tester) async {
+      final SemanticsHandle semantics = tester.ensureSemantics();
+      currentDate = DateTime(2016, DateTime.january, 30);
+      await preparePicker(tester, (Future<DateTimeRange?> range) async {
+        expect(
+          tester.getSemantics(find.text('30')),
+          matchesSemantics(
+            label: '30, Saturday, January 30, 2016, Today',
+            hasTapAction: true,
+            isFocusable: true,
+          ),
+        );
+      });
+      semantics.dispose();
     });
   });
 }
diff --git a/packages/flutter/test/material/dialog_test.dart b/packages/flutter/test/material/dialog_test.dart
index 5b734dda0dd7c9ab7a69002f044232639f3590e1..e04daf090b35f01055e5cd93bb4c95d4baa31bd8 100644
--- a/packages/flutter/test/material/dialog_test.dart
+++ b/packages/flutter/test/material/dialog_test.dart
@@ -2505,6 +2505,7 @@ void main() {
       label: 'Custom label',
       flags: <SemanticsFlag>[SemanticsFlag.namesRoute],
     )));
+    semantics.dispose();
   });
 
   testWidgets('DialogRoute is state restorable', (WidgetTester tester) async {
diff --git a/packages/flutter/test/material/input_date_picker_form_field_test.dart b/packages/flutter/test/material/input_date_picker_form_field_test.dart
index 0cfbd880431afa5cdcc8358fbc74ec6d8e5c1a9c..a07ac58f41944c7b6d97f5542ae0167ce7fea248 100644
--- a/packages/flutter/test/material/input_date_picker_form_field_test.dart
+++ b/packages/flutter/test/material/input_date_picker_form_field_test.dart
@@ -262,7 +262,6 @@ void main() {
 
     testWidgets('Semantics', (WidgetTester tester) async {
       final SemanticsHandle semantics = tester.ensureSemantics();
-      addTearDown(semantics.dispose);
 
       // Fill the clipboard so that the Paste option is available in the text
       // selection menu.
@@ -287,6 +286,7 @@ void main() {
         hasMoveCursorBackwardByCharacterAction: true,
         hasMoveCursorBackwardByWordAction: true,
       ));
+      semantics.dispose();
     });
 
     testWidgets('InputDecorationTheme is honored', (WidgetTester tester) async {
diff --git a/packages/flutter/test/material/text_field_test.dart b/packages/flutter/test/material/text_field_test.dart
index fada6046e21f055485a89b87b8d846fbc8b0b7b3..f742bede79638bca216657c129428dc1994e7041 100644
--- a/packages/flutter/test/material/text_field_test.dart
+++ b/packages/flutter/test/material/text_field_test.dart
@@ -4514,6 +4514,7 @@ void main() {
         ),
       ],
     ), ignoreTransform: true, ignoreRect: true));
+    semantics.dispose();
   });
 
   testWidgets('TextField with specified suffixStyle', (WidgetTester tester) async {
diff --git a/packages/flutter/test/semantics/semantics_owner_test.dart b/packages/flutter/test/semantics/semantics_owner_test.dart
index 142e847575a824d73fd8a24b59e79cd5e8a6b135..4b87fa60eb07adf757ce1a2d06fba8c916e75ce4 100644
--- a/packages/flutter/test/semantics/semantics_owner_test.dart
+++ b/packages/flutter/test/semantics/semantics_owner_test.dart
@@ -13,7 +13,6 @@ void main() {
     // Regression test for https://github.com/flutter/flutter/issues/100358.
 
     final SemanticsTester semantics = SemanticsTester(tester);
-    addTearDown(semantics.dispose);
 
     await tester.pumpWidget(
       Directionality(
@@ -44,5 +43,6 @@ void main() {
       type: SemanticsAction.showOnScreen,
       nodeId: nodeId,
     ));
+    semantics.dispose();
   });
 }
diff --git a/packages/flutter/test/semantics/semantics_test.dart b/packages/flutter/test/semantics/semantics_test.dart
index 16a97847f0c0866f06b1196abfb890ba01222856..72a1647ac5d93ec23803e7e2450db750b435ac54 100644
--- a/packages/flutter/test/semantics/semantics_test.dart
+++ b/packages/flutter/test/semantics/semantics_test.dart
@@ -273,7 +273,8 @@ void main() {
     });
 
     test('after markNeedsSemanticsUpdate() all render objects between two semantic boundaries are asked for annotations', () {
-      TestRenderingFlutterBinding.instance.pipelineOwner.ensureSemantics();
+      final SemanticsHandle handle = TestRenderingFlutterBinding.instance.ensureSemantics();
+      addTearDown(handle.dispose);
 
       TestRender middle;
       final TestRender root = TestRender(
diff --git a/packages/flutter/test/widgets/editable_text_test.dart b/packages/flutter/test/widgets/editable_text_test.dart
index fb63ac51228c2cb9fb4f5fe4eabbf3fd5ac9cf54..34055fa277218c70e2e7c78eeeea6f6cb8a0f65c 100644
--- a/packages/flutter/test/widgets/editable_text_test.dart
+++ b/packages/flutter/test/widgets/editable_text_test.dart
@@ -4510,7 +4510,6 @@ void main() {
 
     testWidgets('are exposed', (WidgetTester tester) async {
       final SemanticsTester semantics = SemanticsTester(tester);
-      addTearDown(semantics.dispose);
 
       controls.testCanCopy = false;
       controls.testCanCut = false;
@@ -4603,6 +4602,7 @@ void main() {
           ],
         ),
       );
+      semantics.dispose();
     });
 
     testWidgets('can copy/cut/paste with a11y', (WidgetTester tester) async {
diff --git a/packages/flutter/test/widgets/focus_scope_test.dart b/packages/flutter/test/widgets/focus_scope_test.dart
index 3edaeb803c20160adbcf6702cc3ee0e03e5a7024..b11590fe8fb3971d1c5935ac17cca0899dc23fbc 100644
--- a/packages/flutter/test/widgets/focus_scope_test.dart
+++ b/packages/flutter/test/widgets/focus_scope_test.dart
@@ -1751,6 +1751,7 @@ void main() {
       await tester.pumpWidget(Focus(includeSemantics: false, child: Container()));
       final TestSemantics expectedSemantics = TestSemantics.root();
       expect(semantics, hasSemantics(expectedSemantics));
+      semantics.dispose();
     });
 
     testWidgets('Focus updates the onKey handler when the widget updates', (WidgetTester tester) async {
@@ -2043,6 +2044,7 @@ void main() {
       await tester.pumpWidget(ExcludeFocus(child: Container()));
       final TestSemantics expectedSemantics = TestSemantics.root();
       expect(semantics, hasSemantics(expectedSemantics));
+      semantics.dispose();
     });
 
     // Regression test for https://github.com/flutter/flutter/issues/92693
diff --git a/packages/flutter/test/widgets/focus_traversal_test.dart b/packages/flutter/test/widgets/focus_traversal_test.dart
index fdb4bce3cff8a776b8cbc5a342d247262437cfbc..2e04bec05eddb95a8940502b8b5f02da6a3b8975 100644
--- a/packages/flutter/test/widgets/focus_traversal_test.dart
+++ b/packages/flutter/test/widgets/focus_traversal_test.dart
@@ -2160,6 +2160,7 @@ void main() {
       await tester.pumpWidget(FocusTraversalGroup(child: Container()));
       final TestSemantics expectedSemantics = TestSemantics.root();
       expect(semantics, hasSemantics(expectedSemantics));
+      semantics.dispose();
     });
 
     testWidgets("Descendants of FocusTraversalGroup aren't focusable if descendantsAreFocusable is false.", (WidgetTester tester) async {
@@ -2418,6 +2419,7 @@ void main() {
         ignoreRect: true,
         ignoreTransform: true,
       ));
+      semantics.dispose();
     });
 
     testWidgets("Raw keyboard listener doesn't introduce a Semantics node when specified", (WidgetTester tester) async {
@@ -2432,6 +2434,7 @@ void main() {
       );
       final TestSemantics expectedSemantics = TestSemantics.root();
       expect(semantics, hasSemantics(expectedSemantics));
+      semantics.dispose();
     });
   });
 
@@ -2489,6 +2492,7 @@ void main() {
       await tester.pumpWidget(ExcludeFocusTraversal(child: Container()));
       final TestSemantics expectedSemantics = TestSemantics.root();
       expect(semantics, hasSemantics(expectedSemantics));
+      semantics.dispose();
     });
   });
 
diff --git a/packages/flutter/test/widgets/selectable_text_test.dart b/packages/flutter/test/widgets/selectable_text_test.dart
index b88cbeea3358df22739c0f1b94d08cfcc8018a28..291671ecb8138ebcd18fce4e3a8372a3bf5a909c 100644
--- a/packages/flutter/test/widgets/selectable_text_test.dart
+++ b/packages/flutter/test/widgets/selectable_text_test.dart
@@ -2297,6 +2297,7 @@ void main() {
         ),
       ],
     ), ignoreTransform: true, ignoreRect: true));
+    semantics.dispose();
   });
 
   testWidgets('SelectableText semantics, enableInteractiveSelection = false', (WidgetTester tester) async {
diff --git a/packages/flutter/test/widgets/semantics_child_configs_delegate_test.dart b/packages/flutter/test/widgets/semantics_child_configs_delegate_test.dart
index be9ba69ea31fc94bb8c1f2016a32417a67f8fa2c..33f3b282781762b479995544f6406fbd6fc046b2 100644
--- a/packages/flutter/test/widgets/semantics_child_configs_delegate_test.dart
+++ b/packages/flutter/test/widgets/semantics_child_configs_delegate_test.dart
@@ -71,6 +71,7 @@ void main() {
         ),
       ],
     ), ignoreId: true, ignoreRect: true, ignoreTransform: true));
+    semantics.dispose();
   });
 
   testWidgets('Semantics can drop semantics config', (WidgetTester tester) async {
@@ -128,6 +129,7 @@ void main() {
         ),
       ],
     ), ignoreId: true, ignoreRect: true, ignoreTransform: true));
+    semantics.dispose();
   });
 
   testWidgets('Semantics throws when mark the same config twice case 1', (WidgetTester tester) async {
diff --git a/packages/flutter/test/widgets/semantics_tester.dart b/packages/flutter/test/widgets/semantics_tester.dart
index 0d18f240df7efdc8ac31c1bfa7f5838ecafc1399..532133b8ba2cce4ac8f79e9bc6f67e58ddc0c4d7 100644
--- a/packages/flutter/test/widgets/semantics_tester.dart
+++ b/packages/flutter/test/widgets/semantics_tester.dart
@@ -421,7 +421,7 @@ class SemanticsTester {
   /// You should call [dispose] at the end of a test that creates a semantics
   /// tester.
   SemanticsTester(this.tester) {
-    _semanticsHandle = tester.binding.pipelineOwner.ensureSemantics();
+    _semanticsHandle = tester.ensureSemantics();
 
     // This _extra_ clean-up is needed for the case when a test fails and
     // therefore fails to call dispose() explicitly. The test is still required
diff --git a/packages/flutter/test/widgets/slivers_appbar_floating_pinned_test.dart b/packages/flutter/test/widgets/slivers_appbar_floating_pinned_test.dart
index 7d22fc79413a33421db16249f03602389abde4b0..4831cb697d23ae0853c165360c6b090c0b8889a9 100644
--- a/packages/flutter/test/widgets/slivers_appbar_floating_pinned_test.dart
+++ b/packages/flutter/test/widgets/slivers_appbar_floating_pinned_test.dart
@@ -238,6 +238,7 @@ void main() {
       ],
     );
     expect(semantics, hasSemantics(expectedSemantics, ignoreTransform: true, ignoreId: true, ignoreRect: true));
+    semantics.dispose();
   });
 
   testWidgets('Sliver appbars - floating and pinned - second app bar stacks below', (WidgetTester tester) async {
diff --git a/packages/flutter/test/widgets/slivers_test.dart b/packages/flutter/test/widgets/slivers_test.dart
index bd9231afd2ccd257036ce9fce1015fd08397470c..9c6d287c0c4d4a889eb2b19bb0fe615deba6ec11 100644
--- a/packages/flutter/test/widgets/slivers_test.dart
+++ b/packages/flutter/test/widgets/slivers_test.dart
@@ -677,6 +677,7 @@ void main() {
       final RenderSliver renderSliver = renderViewport.lastChild!;
       expect(renderSliver.geometry!.scrollExtent, 0.0);
       expect(find.byType(SliverOffstage), findsNothing);
+      semantics.dispose();
     });
 
     testWidgets('offstage false', (WidgetTester tester) async {
@@ -696,6 +697,7 @@ void main() {
       final RenderSliver renderSliver = renderViewport.lastChild!;
       expect(renderSliver.geometry!.scrollExtent, 14.0);
       expect(find.byType(SliverOffstage), paints..paragraph());
+      semantics.dispose();
     });
   });
 
@@ -841,6 +843,7 @@ void main() {
       expect(semantics.nodesWith(label: 'a'), hasLength(1));
       await tester.tap(find.byType(GestureDetector), warnIfMissed: false);
       expect(events, equals(<String>[]));
+      semantics.dispose();
     });
 
     testWidgets('ignores semantics', (WidgetTester tester) async {
@@ -863,6 +866,7 @@ void main() {
       expect(semantics.nodesWith(label: 'a'), hasLength(0));
       await tester.tap(find.byType(GestureDetector));
       expect(events, equals(<String>['tap']));
+      semantics.dispose();
     });
 
     testWidgets('ignores pointer events & semantics', (WidgetTester tester) async {
@@ -884,6 +888,7 @@ void main() {
       expect(semantics.nodesWith(label: 'a'), hasLength(0));
       await tester.tap(find.byType(GestureDetector), warnIfMissed: false);
       expect(events, equals(<String>[]));
+      semantics.dispose();
     });
 
     testWidgets('ignores nothing', (WidgetTester tester) async {
@@ -906,6 +911,7 @@ void main() {
       expect(semantics.nodesWith(label: 'a'), hasLength(1));
       await tester.tap(find.byType(GestureDetector));
       expect(events, equals(<String>['tap']));
+      semantics.dispose();
     });
   });
 
diff --git a/packages/flutter/test/widgets/text_test.dart b/packages/flutter/test/widgets/text_test.dart
index 39ca2ac0ace68bfe87b894007c2739920f85fc67..1f1efbdc18fc65f26a7a8fc01744e048fa0b792a 100644
--- a/packages/flutter/test/widgets/text_test.dart
+++ b/packages/flutter/test/widgets/text_test.dart
@@ -1151,6 +1151,7 @@ void main() {
         ),
       ],
     )));
+    semantics.dispose();
   }, skip: isBrowser); // https://github.com/flutter/flutter/issues/87877
 
   // Regression test for https://github.com/flutter/flutter/issues/69787
@@ -1174,29 +1175,34 @@ void main() {
       ),
     );
 
-    expect(semantics, hasSemantics(TestSemantics.root(
-      children: <TestSemantics>[
-        TestSemantics(
+    expect(
+      semantics,
+      hasSemantics(
+        TestSemantics.root(
           children: <TestSemantics>[
-            TestSemantics(label: 'included'),
             TestSemantics(
-              label: 'HELLO',
-              actions: <SemanticsAction>[
-                SemanticsAction.tap,
-              ],
-              flags: <SemanticsFlag>[
-                SemanticsFlag.isLink,
+              children: <TestSemantics>[
+                TestSemantics(label: 'included'),
+                TestSemantics(
+                  label: 'HELLO',
+                  actions: <SemanticsAction>[
+                    SemanticsAction.tap,
+                  ],
+                  flags: <SemanticsFlag>[
+                    SemanticsFlag.isLink,
+                  ],
+                ),
+                TestSemantics(label: 'included2'),
               ],
             ),
-            TestSemantics(label: 'included2'),
           ],
         ),
-      ],
-    ),
-    ignoreId: true,
-    ignoreRect: true,
-    ignoreTransform: true,
-    ));
+        ignoreId: true,
+        ignoreRect: true,
+        ignoreTransform: true,
+      ),
+    );
+    semantics.dispose();
   }, skip: isBrowser); // https://github.com/flutter/flutter/issues/87877
 
   // Regression test for https://github.com/flutter/flutter/issues/69787
@@ -1232,29 +1238,34 @@ void main() {
       ),
     );
 
-    expect(semantics, hasSemantics(TestSemantics.root(
-      children: <TestSemantics>[
-        TestSemantics(
+    expect(
+      semantics,
+      hasSemantics(
+        TestSemantics.root(
           children: <TestSemantics>[
-            TestSemantics(label: 'foo'),
-            TestSemantics(label: 'bar'),
             TestSemantics(
-              label: 'HELLO',
-              actions: <SemanticsAction>[
-                SemanticsAction.tap,
-              ],
-              flags: <SemanticsFlag>[
-                SemanticsFlag.isLink,
+              children: <TestSemantics>[
+                TestSemantics(label: 'foo'),
+                TestSemantics(label: 'bar'),
+                TestSemantics(
+                  label: 'HELLO',
+                  actions: <SemanticsAction>[
+                    SemanticsAction.tap,
+                  ],
+                  flags: <SemanticsFlag>[
+                    SemanticsFlag.isLink,
+                  ],
+                ),
               ],
             ),
           ],
         ),
-      ],
-    ),
-    ignoreId: true,
-    ignoreRect: true,
-    ignoreTransform: true,
-    ));
+        ignoreId: true,
+        ignoreRect: true,
+        ignoreTransform: true,
+      ),
+    );
+    semantics.dispose();
   }, skip: isBrowser); // https://github.com/flutter/flutter/issues/87877
 
   // Regression test for https://github.com/flutter/flutter/issues/69787
@@ -1303,24 +1314,29 @@ void main() {
       ),
     );
 
-    expect(semantics, hasSemantics(TestSemantics.root(
-      children: <TestSemantics>[
-        TestSemantics(
+    expect(
+      semantics,
+      hasSemantics(
+        TestSemantics.root(
           children: <TestSemantics>[
-            TestSemantics(label: 'not clipped'),
             TestSemantics(
-              label: 'next WS is clipped',
-              flags: <SemanticsFlag>[SemanticsFlag.isLink],
-              actions: <SemanticsAction>[SemanticsAction.tap],
+              children: <TestSemantics>[
+                TestSemantics(label: 'not clipped'),
+                TestSemantics(
+                  label: 'next WS is clipped',
+                  flags: <SemanticsFlag>[SemanticsFlag.isLink],
+                  actions: <SemanticsAction>[SemanticsAction.tap],
+                ),
+              ],
             ),
           ],
         ),
-      ],
-    ),
-    ignoreId: true,
-    ignoreRect: true,
-    ignoreTransform: true,
-    ));
+        ignoreId: true,
+        ignoreRect: true,
+        ignoreTransform: true,
+      ),
+    );
+    semantics.dispose();
   }, skip: isBrowser); // https://github.com/flutter/flutter/issues/87877
 
   testWidgets('RenderParagraph intrinsic width', (WidgetTester tester) async {
diff --git a/packages/flutter_driver/test/src/real_tests/extension_test.dart b/packages/flutter_driver/test/src/real_tests/extension_test.dart
index 1756738f1cdcf0f34fc5ba3768b4fbfa2e1be2ab..361204716bb6814f9b48bf3c33c1a8105069a5a2 100644
--- a/packages/flutter_driver/test/src/real_tests/extension_test.dart
+++ b/packages/flutter_driver/test/src/real_tests/extension_test.dart
@@ -473,7 +473,7 @@ void main() {
     });
 
     testWidgets('works when semantics are enabled', (WidgetTester tester) async {
-      final SemanticsHandle semantics = RendererBinding.instance.pipelineOwner.ensureSemantics();
+      final SemanticsHandle semantics = tester.ensureSemantics();
       await tester.pumpWidget(
         const Text('hello', textDirection: TextDirection.ltr));
 
@@ -497,7 +497,7 @@ void main() {
     }, semanticsEnabled: false);
 
     testWidgets('throws state error multiple matches are found', (WidgetTester tester) async {
-      final SemanticsHandle semantics = RendererBinding.instance.pipelineOwner.ensureSemantics();
+      final SemanticsHandle semantics = tester.ensureSemantics();
       await tester.pumpWidget(
         Directionality(
           textDirection: TextDirection.ltr,
diff --git a/packages/flutter_test/lib/src/widget_tester.dart b/packages/flutter_test/lib/src/widget_tester.dart
index e82291c320316bb9f2126ed88c09ded2eb56fe6c..af24daf5b6303cd29eb0c4cd4444a09eadcaff80 100644
--- a/packages/flutter_test/lib/src/widget_tester.dart
+++ b/packages/flutter_test/lib/src/widget_tester.dart
@@ -155,10 +155,10 @@ void testWidgets(
       () {
         tester._testDescription = combinedDescription;
         SemanticsHandle? semanticsHandle;
+        tester._recordNumberOfSemanticsHandles();
         if (semanticsEnabled == true) {
           semanticsHandle = tester.ensureSemantics();
         }
-        tester._recordNumberOfSemanticsHandles();
         test_package.addTearDown(binding.postTest);
         return binding.runTest(
           () async {
@@ -1044,18 +1044,16 @@ class WidgetTester extends WidgetController implements HitTestDispatcher, Ticker
 
   void _verifySemanticsHandlesWereDisposed() {
     assert(_lastRecordedSemanticsHandles != null);
-    if (binding.pipelineOwner.debugOutstandingSemanticsHandles > _lastRecordedSemanticsHandles!) {
+    // TODO(goderbauer): Fix known leak in web engine when running integration tests and remove this "correction", https://github.com/flutter/flutter/issues/121640.
+    final int knownWebEngineLeakForLiveTestsCorrection = kIsWeb && binding is LiveTestWidgetsFlutterBinding ? 2 : 0;
+
+    if (_currentSemanticsHandles - knownWebEngineLeakForLiveTestsCorrection > _lastRecordedSemanticsHandles!) {
       throw FlutterError.fromParts(<DiagnosticsNode>[
         ErrorSummary('A SemanticsHandle was active at the end of the test.'),
         ErrorDescription(
           'All SemanticsHandle instances must be disposed by calling dispose() on '
           'the SemanticsHandle.'
         ),
-        ErrorHint(
-          'If your test uses SemanticsTester, it is '
-          'sufficient to call dispose() on SemanticsTester. Otherwise, the '
-          'existing handle will leak into another test and alter its behavior.'
-        ),
       ]);
     }
     _lastRecordedSemanticsHandles = null;
@@ -1063,8 +1061,10 @@ class WidgetTester extends WidgetController implements HitTestDispatcher, Ticker
 
   int? _lastRecordedSemanticsHandles;
 
+  int get _currentSemanticsHandles => binding.debugOutstandingSemanticsHandles + binding.pipelineOwner.debugOutstandingSemanticsHandles;
+
   void _recordNumberOfSemanticsHandles() {
-    _lastRecordedSemanticsHandles = binding.pipelineOwner.debugOutstandingSemanticsHandles;
+    _lastRecordedSemanticsHandles = _currentSemanticsHandles;
   }
 
   /// Returns the TestTextInput singleton.
diff --git a/packages/flutter_test/test/matchers_test.dart b/packages/flutter_test/test/matchers_test.dart
index 7086a8a479d381b4cb34221deada61523b328463..82942bb17ebe886ec078cb5f29b5a87fbb69df06 100644
--- a/packages/flutter_test/test/matchers_test.dart
+++ b/packages/flutter_test/test/matchers_test.dart
@@ -737,7 +737,6 @@ void main() {
 
     testWidgets('failure does not throw unexpected errors', (WidgetTester tester) async {
       final SemanticsHandle handle = tester.ensureSemantics();
-      addTearDown(() => handle.dispose());
 
       const Key key = Key('semantics');
       await tester.pumpWidget(Semantics(
@@ -789,13 +788,13 @@ void main() {
       );
 
       expect(failedExpectation, throwsA(isA<TestFailure>()));
+      handle.dispose();
     });
   });
 
   group('containsSemantics', () {
     testWidgets('matches SemanticsData', (WidgetTester tester) async {
       final SemanticsHandle handle = tester.ensureSemantics();
-      addTearDown(() => handle.dispose());
 
       const Key key = Key('semantics');
       await tester.pumpWidget(Semantics(
@@ -889,6 +888,7 @@ void main() {
         )),
         reason: 'onTapHint "scans" should not have matched "scan".',
       );
+      handle.dispose();
     });
 
     testWidgets('can match all semantics flags and actions enabled', (WidgetTester tester) async {
@@ -1233,7 +1233,6 @@ void main() {
 
     testWidgets('failure does not throw unexpected errors', (WidgetTester tester) async {
       final SemanticsHandle handle = tester.ensureSemantics();
-      addTearDown(() => handle.dispose());
 
       const Key key = Key('semantics');
       await tester.pumpWidget(Semantics(
@@ -1283,6 +1282,7 @@ void main() {
       );
 
       expect(failedExpectation, throwsA(isA<TestFailure>()));
+      handle.dispose();
     });
   });
 
diff --git a/packages/flutter_test/test/semantics_checker/flutter_test_config.dart b/packages/flutter_test/test/semantics_checker/flutter_test_config.dart
new file mode 100644
index 0000000000000000000000000000000000000000..88b2aee380d1ad86d716dd153d1e460ab8cf807d
--- /dev/null
+++ b/packages/flutter_test/test/semantics_checker/flutter_test_config.dart
@@ -0,0 +1,71 @@
+// 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 'dart:async';
+
+import 'package:flutter/foundation.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+final List<FlutterErrorDetails> errors = <FlutterErrorDetails>[];
+
+Future<void> testExecutable(FutureOr<void> Function() testMain) async {
+  reportTestException = (FlutterErrorDetails details, String testDescription) {
+    errors.add(details);
+  };
+
+  // The error that the test throws in their run methods below will be forwarded
+  // to our exception handler above and do not cause the test to fail. The
+  // tearDown method then checks that the test threw the expected exception.
+  await testMain();
+}
+
+void pipelineOwnerTestRun() {
+  testWidgets('open SemanticsHandle from PipelineOwner fails test', (WidgetTester tester) async {
+    final int outstandingHandles = tester.binding.pipelineOwner.debugOutstandingSemanticsHandles;
+    tester.binding.pipelineOwner.ensureSemantics();
+    expect(tester.binding.pipelineOwner.debugOutstandingSemanticsHandles, outstandingHandles + 1);
+    // SemanticsHandle is not disposed on purpose to verify in tearDown that
+    // the test failed due to an active SemanticsHandle.
+  });
+
+  tearDown(() {
+    expect(errors, hasLength(1));
+    expect(errors.single.toString(), contains('SemanticsHandle was active at the end of the test'));
+  });
+}
+
+void semanticsBindingTestRun() {
+  testWidgets('open SemanticsHandle from SemanticsBinding fails test', (WidgetTester tester) async {
+    final int outstandingHandles = tester.binding.debugOutstandingSemanticsHandles;
+    tester.binding.ensureSemantics();
+    expect(tester.binding.debugOutstandingSemanticsHandles, outstandingHandles + 1);
+    // SemanticsHandle is not disposed on purpose to verify in tearDown that
+    // the test failed due to an active SemanticsHandle.
+  });
+
+  tearDown(() {
+    expect(errors, hasLength(1));
+    expect(errors.single.toString(), contains('SemanticsHandle was active at the end of the test'));
+  });
+}
+
+void failingTestTestRun() {
+  testWidgets('open SemanticsHandle from SemanticsBinding fails test', (WidgetTester tester) async {
+    final int outstandingHandles = tester.binding.debugOutstandingSemanticsHandles;
+    tester.binding.ensureSemantics();
+    expect(tester.binding.debugOutstandingSemanticsHandles, outstandingHandles + 1);
+
+    // Failing expectation to verify that an open semantics handle doesn't
+    // cause any cascading failures and only the failing expectation is
+    // reported.
+    expect(1, equals(2));
+    fail('The test should never have gotten this far.');
+  });
+
+  tearDown(() {
+    expect(errors, hasLength(1));
+    expect(errors.single.toString(), contains('Expected: <2>'));
+    expect(errors.single.toString(), contains('Actual: <1>'));
+  });
+}
diff --git a/packages/flutter_test/test/semantics_checker/open_handle_with_failing_test.dart b/packages/flutter_test/test/semantics_checker/open_handle_with_failing_test.dart
new file mode 100644
index 0000000000000000000000000000000000000000..574aee890b474751cf07b88a3bba9f6ea8f1e451
--- /dev/null
+++ b/packages/flutter_test/test/semantics_checker/open_handle_with_failing_test.dart
@@ -0,0 +1,7 @@
+// 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 'flutter_test_config.dart' as config;
+
+void main() => config.failingTestTestRun();
diff --git a/packages/flutter_test/test/semantics_checker/pipeline_owner_semantics_handle_test.dart b/packages/flutter_test/test/semantics_checker/pipeline_owner_semantics_handle_test.dart
new file mode 100644
index 0000000000000000000000000000000000000000..9d52ab19ad1104622b6c18bb31823619f21c61ad
--- /dev/null
+++ b/packages/flutter_test/test/semantics_checker/pipeline_owner_semantics_handle_test.dart
@@ -0,0 +1,7 @@
+// 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 'flutter_test_config.dart' as config;
+
+void main() => config.pipelineOwnerTestRun();
diff --git a/packages/flutter_test/test/semantics_checker/semantics_binding_semantics_handle_test.dart b/packages/flutter_test/test/semantics_checker/semantics_binding_semantics_handle_test.dart
new file mode 100644
index 0000000000000000000000000000000000000000..e5e592f1f85c5523a079205664254c8bb2d92298
--- /dev/null
+++ b/packages/flutter_test/test/semantics_checker/semantics_binding_semantics_handle_test.dart
@@ -0,0 +1,7 @@
+// 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 'flutter_test_config.dart' as config;
+
+void main() => config.semanticsBindingTestRun();