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();