Fix how tests count open SemanticsHandles (#121571)

Fix how tests count open SemanticsHandles
This commit is contained in:
Michael Goderbauer 2023-02-28 15:55:58 -08:00 committed by GitHub
parent f97a51533b
commit 6de42a70f1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 219 additions and 85 deletions

View File

@ -67,6 +67,11 @@ mixin SemanticsBinding on BindingBase {
_semanticsEnabled.removeListener(listener); _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; int _outstandingHandles = 0;
/// Creates a new [SemanticsHandle] and requests the collection of semantics /// Creates a new [SemanticsHandle] and requests the collection of semantics

View File

@ -1247,6 +1247,7 @@ void main() {
label: 'Custom label', label: 'Custom label',
flags: <SemanticsFlag>[SemanticsFlag.namesRoute], flags: <SemanticsFlag>[SemanticsFlag.namesRoute],
))); )));
semantics.dispose();
}); });
testWidgets('CupertinoDialogRoute is state restorable', (WidgetTester tester) async { testWidgets('CupertinoDialogRoute is state restorable', (WidgetTester tester) async {

View File

@ -1484,6 +1484,7 @@ void main() {
label: 'Dismiss', label: 'Dismiss',
))); )));
debugDefaultTargetPlatformOverride = null; debugDefaultTargetPlatformOverride = null;
semantics.dispose();
}); });
testWidgets('showCupertinoModalPopup allows for semantics dismiss when set', (WidgetTester tester) async { testWidgets('showCupertinoModalPopup allows for semantics dismiss when set', (WidgetTester tester) async {
@ -1519,6 +1520,7 @@ void main() {
label: 'Dismiss', label: 'Dismiss',
)); ));
debugDefaultTargetPlatformOverride = null; debugDefaultTargetPlatformOverride = null;
semantics.dispose();
}); });
testWidgets('showCupertinoModalPopup passes RouteSettings to PopupRoute', (WidgetTester tester) async { testWidgets('showCupertinoModalPopup passes RouteSettings to PopupRoute', (WidgetTester tester) async {

View File

@ -657,7 +657,6 @@ void main() {
group('Semantics', () { group('Semantics', () {
testWidgets('day mode', (WidgetTester tester) async { testWidgets('day mode', (WidgetTester tester) async {
final SemanticsHandle semantics = tester.ensureSemantics(); final SemanticsHandle semantics = tester.ensureSemantics();
addTearDown(semantics.dispose);
await tester.pumpWidget(calendarDatePicker()); await tester.pumpWidget(calendarDatePicker());
@ -837,11 +836,11 @@ void main() {
hasTapAction: true, hasTapAction: true,
isFocusable: true, isFocusable: true,
)); ));
semantics.dispose();
}); });
testWidgets('calendar year mode', (WidgetTester tester) async { testWidgets('calendar year mode', (WidgetTester tester) async {
final SemanticsHandle semantics = tester.ensureSemantics(); final SemanticsHandle semantics = tester.ensureSemantics();
addTearDown(semantics.dispose);
await tester.pumpWidget(calendarDatePicker( await tester.pumpWidget(calendarDatePicker(
initialCalendarMode: DatePickerMode.year, initialCalendarMode: DatePickerMode.year,
@ -863,8 +862,8 @@ void main() {
isButton: true, isButton: true,
)); ));
} }
semantics.dispose();
}); });
}); });
}); });

View File

@ -814,7 +814,6 @@ void main() {
group('Semantics', () { group('Semantics', () {
testWidgets('calendar mode', (WidgetTester tester) async { testWidgets('calendar mode', (WidgetTester tester) async {
final SemanticsHandle semantics = tester.ensureSemantics(); final SemanticsHandle semantics = tester.ensureSemantics();
addTearDown(semantics.dispose);
await prepareDatePicker(tester, (Future<DateTime?> date) async { await prepareDatePicker(tester, (Future<DateTime?> date) async {
// Header // Header
@ -858,11 +857,11 @@ void main() {
isFocusable: true, isFocusable: true,
)); ));
}); });
semantics.dispose();
}); });
testWidgets('input mode', (WidgetTester tester) async { testWidgets('input mode', (WidgetTester tester) async {
final SemanticsHandle semantics = tester.ensureSemantics(); final SemanticsHandle semantics = tester.ensureSemantics();
addTearDown(semantics.dispose);
initialEntryMode = DatePickerEntryMode.input; initialEntryMode = DatePickerEntryMode.input;
await prepareDatePicker(tester, (Future<DateTime?> date) async { await prepareDatePicker(tester, (Future<DateTime?> date) async {
@ -901,6 +900,7 @@ void main() {
isFocusable: true, isFocusable: true,
)); ));
}); });
semantics.dispose();
}); });
}); });

View File

@ -1086,21 +1086,21 @@ void main() {
}); });
}); });
group('Semantics', () { group('Semantics', () {
testWidgets('calendar mode', (WidgetTester tester) async { testWidgets('calendar mode', (WidgetTester tester) async {
final SemanticsHandle semantics = tester.ensureSemantics(); final SemanticsHandle semantics = tester.ensureSemantics();
currentDate = DateTime(2016, DateTime.january, 30); currentDate = DateTime(2016, DateTime.january, 30);
addTearDown(semantics.dispose); await preparePicker(tester, (Future<DateTimeRange?> range) async {
await preparePicker(tester, (Future<DateTimeRange?> range) async { expect(
expect( tester.getSemantics(find.text('30')),
tester.getSemantics(find.text('30')), matchesSemantics(
matchesSemantics( label: '30, Saturday, January 30, 2016, Today',
label: '30, Saturday, January 30, 2016, Today', hasTapAction: true,
hasTapAction: true, isFocusable: true,
isFocusable: true, ),
), );
); });
}); semantics.dispose();
}); });
}); });
} }

View File

@ -2505,6 +2505,7 @@ void main() {
label: 'Custom label', label: 'Custom label',
flags: <SemanticsFlag>[SemanticsFlag.namesRoute], flags: <SemanticsFlag>[SemanticsFlag.namesRoute],
))); )));
semantics.dispose();
}); });
testWidgets('DialogRoute is state restorable', (WidgetTester tester) async { testWidgets('DialogRoute is state restorable', (WidgetTester tester) async {

View File

@ -262,7 +262,6 @@ void main() {
testWidgets('Semantics', (WidgetTester tester) async { testWidgets('Semantics', (WidgetTester tester) async {
final SemanticsHandle semantics = tester.ensureSemantics(); final SemanticsHandle semantics = tester.ensureSemantics();
addTearDown(semantics.dispose);
// Fill the clipboard so that the Paste option is available in the text // Fill the clipboard so that the Paste option is available in the text
// selection menu. // selection menu.
@ -287,6 +286,7 @@ void main() {
hasMoveCursorBackwardByCharacterAction: true, hasMoveCursorBackwardByCharacterAction: true,
hasMoveCursorBackwardByWordAction: true, hasMoveCursorBackwardByWordAction: true,
)); ));
semantics.dispose();
}); });
testWidgets('InputDecorationTheme is honored', (WidgetTester tester) async { testWidgets('InputDecorationTheme is honored', (WidgetTester tester) async {

View File

@ -4514,6 +4514,7 @@ void main() {
), ),
], ],
), ignoreTransform: true, ignoreRect: true)); ), ignoreTransform: true, ignoreRect: true));
semantics.dispose();
}); });
testWidgets('TextField with specified suffixStyle', (WidgetTester tester) async { testWidgets('TextField with specified suffixStyle', (WidgetTester tester) async {

View File

@ -13,7 +13,6 @@ void main() {
// Regression test for https://github.com/flutter/flutter/issues/100358. // Regression test for https://github.com/flutter/flutter/issues/100358.
final SemanticsTester semantics = SemanticsTester(tester); final SemanticsTester semantics = SemanticsTester(tester);
addTearDown(semantics.dispose);
await tester.pumpWidget( await tester.pumpWidget(
Directionality( Directionality(
@ -44,5 +43,6 @@ void main() {
type: SemanticsAction.showOnScreen, type: SemanticsAction.showOnScreen,
nodeId: nodeId, nodeId: nodeId,
)); ));
semantics.dispose();
}); });
} }

View File

@ -273,7 +273,8 @@ void main() {
}); });
test('after markNeedsSemanticsUpdate() all render objects between two semantic boundaries are asked for annotations', () { 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; TestRender middle;
final TestRender root = TestRender( final TestRender root = TestRender(

View File

@ -4510,7 +4510,6 @@ void main() {
testWidgets('are exposed', (WidgetTester tester) async { testWidgets('are exposed', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester); final SemanticsTester semantics = SemanticsTester(tester);
addTearDown(semantics.dispose);
controls.testCanCopy = false; controls.testCanCopy = false;
controls.testCanCut = false; controls.testCanCut = false;
@ -4603,6 +4602,7 @@ void main() {
], ],
), ),
); );
semantics.dispose();
}); });
testWidgets('can copy/cut/paste with a11y', (WidgetTester tester) async { testWidgets('can copy/cut/paste with a11y', (WidgetTester tester) async {

View File

@ -1751,6 +1751,7 @@ void main() {
await tester.pumpWidget(Focus(includeSemantics: false, child: Container())); await tester.pumpWidget(Focus(includeSemantics: false, child: Container()));
final TestSemantics expectedSemantics = TestSemantics.root(); final TestSemantics expectedSemantics = TestSemantics.root();
expect(semantics, hasSemantics(expectedSemantics)); expect(semantics, hasSemantics(expectedSemantics));
semantics.dispose();
}); });
testWidgets('Focus updates the onKey handler when the widget updates', (WidgetTester tester) async { 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())); await tester.pumpWidget(ExcludeFocus(child: Container()));
final TestSemantics expectedSemantics = TestSemantics.root(); final TestSemantics expectedSemantics = TestSemantics.root();
expect(semantics, hasSemantics(expectedSemantics)); expect(semantics, hasSemantics(expectedSemantics));
semantics.dispose();
}); });
// Regression test for https://github.com/flutter/flutter/issues/92693 // Regression test for https://github.com/flutter/flutter/issues/92693

View File

@ -2160,6 +2160,7 @@ void main() {
await tester.pumpWidget(FocusTraversalGroup(child: Container())); await tester.pumpWidget(FocusTraversalGroup(child: Container()));
final TestSemantics expectedSemantics = TestSemantics.root(); final TestSemantics expectedSemantics = TestSemantics.root();
expect(semantics, hasSemantics(expectedSemantics)); expect(semantics, hasSemantics(expectedSemantics));
semantics.dispose();
}); });
testWidgets("Descendants of FocusTraversalGroup aren't focusable if descendantsAreFocusable is false.", (WidgetTester tester) async { testWidgets("Descendants of FocusTraversalGroup aren't focusable if descendantsAreFocusable is false.", (WidgetTester tester) async {
@ -2418,6 +2419,7 @@ void main() {
ignoreRect: true, ignoreRect: true,
ignoreTransform: true, ignoreTransform: true,
)); ));
semantics.dispose();
}); });
testWidgets("Raw keyboard listener doesn't introduce a Semantics node when specified", (WidgetTester tester) async { 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(); final TestSemantics expectedSemantics = TestSemantics.root();
expect(semantics, hasSemantics(expectedSemantics)); expect(semantics, hasSemantics(expectedSemantics));
semantics.dispose();
}); });
}); });
@ -2489,6 +2492,7 @@ void main() {
await tester.pumpWidget(ExcludeFocusTraversal(child: Container())); await tester.pumpWidget(ExcludeFocusTraversal(child: Container()));
final TestSemantics expectedSemantics = TestSemantics.root(); final TestSemantics expectedSemantics = TestSemantics.root();
expect(semantics, hasSemantics(expectedSemantics)); expect(semantics, hasSemantics(expectedSemantics));
semantics.dispose();
}); });
}); });

View File

@ -2297,6 +2297,7 @@ void main() {
), ),
], ],
), ignoreTransform: true, ignoreRect: true)); ), ignoreTransform: true, ignoreRect: true));
semantics.dispose();
}); });
testWidgets('SelectableText semantics, enableInteractiveSelection = false', (WidgetTester tester) async { testWidgets('SelectableText semantics, enableInteractiveSelection = false', (WidgetTester tester) async {

View File

@ -71,6 +71,7 @@ void main() {
), ),
], ],
), ignoreId: true, ignoreRect: true, ignoreTransform: true)); ), ignoreId: true, ignoreRect: true, ignoreTransform: true));
semantics.dispose();
}); });
testWidgets('Semantics can drop semantics config', (WidgetTester tester) async { testWidgets('Semantics can drop semantics config', (WidgetTester tester) async {
@ -128,6 +129,7 @@ void main() {
), ),
], ],
), ignoreId: true, ignoreRect: true, ignoreTransform: true)); ), ignoreId: true, ignoreRect: true, ignoreTransform: true));
semantics.dispose();
}); });
testWidgets('Semantics throws when mark the same config twice case 1', (WidgetTester tester) async { testWidgets('Semantics throws when mark the same config twice case 1', (WidgetTester tester) async {

View File

@ -421,7 +421,7 @@ class SemanticsTester {
/// You should call [dispose] at the end of a test that creates a semantics /// You should call [dispose] at the end of a test that creates a semantics
/// tester. /// tester.
SemanticsTester(this.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 // 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 // therefore fails to call dispose() explicitly. The test is still required

View File

@ -238,6 +238,7 @@ void main() {
], ],
); );
expect(semantics, hasSemantics(expectedSemantics, ignoreTransform: true, ignoreId: true, ignoreRect: true)); 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 { testWidgets('Sliver appbars - floating and pinned - second app bar stacks below', (WidgetTester tester) async {

View File

@ -677,6 +677,7 @@ void main() {
final RenderSliver renderSliver = renderViewport.lastChild!; final RenderSliver renderSliver = renderViewport.lastChild!;
expect(renderSliver.geometry!.scrollExtent, 0.0); expect(renderSliver.geometry!.scrollExtent, 0.0);
expect(find.byType(SliverOffstage), findsNothing); expect(find.byType(SliverOffstage), findsNothing);
semantics.dispose();
}); });
testWidgets('offstage false', (WidgetTester tester) async { testWidgets('offstage false', (WidgetTester tester) async {
@ -696,6 +697,7 @@ void main() {
final RenderSliver renderSliver = renderViewport.lastChild!; final RenderSliver renderSliver = renderViewport.lastChild!;
expect(renderSliver.geometry!.scrollExtent, 14.0); expect(renderSliver.geometry!.scrollExtent, 14.0);
expect(find.byType(SliverOffstage), paints..paragraph()); expect(find.byType(SliverOffstage), paints..paragraph());
semantics.dispose();
}); });
}); });
@ -841,6 +843,7 @@ void main() {
expect(semantics.nodesWith(label: 'a'), hasLength(1)); expect(semantics.nodesWith(label: 'a'), hasLength(1));
await tester.tap(find.byType(GestureDetector), warnIfMissed: false); await tester.tap(find.byType(GestureDetector), warnIfMissed: false);
expect(events, equals(<String>[])); expect(events, equals(<String>[]));
semantics.dispose();
}); });
testWidgets('ignores semantics', (WidgetTester tester) async { testWidgets('ignores semantics', (WidgetTester tester) async {
@ -863,6 +866,7 @@ void main() {
expect(semantics.nodesWith(label: 'a'), hasLength(0)); expect(semantics.nodesWith(label: 'a'), hasLength(0));
await tester.tap(find.byType(GestureDetector)); await tester.tap(find.byType(GestureDetector));
expect(events, equals(<String>['tap'])); expect(events, equals(<String>['tap']));
semantics.dispose();
}); });
testWidgets('ignores pointer events & semantics', (WidgetTester tester) async { testWidgets('ignores pointer events & semantics', (WidgetTester tester) async {
@ -884,6 +888,7 @@ void main() {
expect(semantics.nodesWith(label: 'a'), hasLength(0)); expect(semantics.nodesWith(label: 'a'), hasLength(0));
await tester.tap(find.byType(GestureDetector), warnIfMissed: false); await tester.tap(find.byType(GestureDetector), warnIfMissed: false);
expect(events, equals(<String>[])); expect(events, equals(<String>[]));
semantics.dispose();
}); });
testWidgets('ignores nothing', (WidgetTester tester) async { testWidgets('ignores nothing', (WidgetTester tester) async {
@ -906,6 +911,7 @@ void main() {
expect(semantics.nodesWith(label: 'a'), hasLength(1)); expect(semantics.nodesWith(label: 'a'), hasLength(1));
await tester.tap(find.byType(GestureDetector)); await tester.tap(find.byType(GestureDetector));
expect(events, equals(<String>['tap'])); expect(events, equals(<String>['tap']));
semantics.dispose();
}); });
}); });

View File

@ -1151,6 +1151,7 @@ void main() {
), ),
], ],
))); )));
semantics.dispose();
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/87877 }, skip: isBrowser); // https://github.com/flutter/flutter/issues/87877
// Regression test for https://github.com/flutter/flutter/issues/69787 // Regression test for https://github.com/flutter/flutter/issues/69787
@ -1174,29 +1175,34 @@ void main() {
), ),
); );
expect(semantics, hasSemantics(TestSemantics.root( expect(
children: <TestSemantics>[ semantics,
TestSemantics( hasSemantics(
TestSemantics.root(
children: <TestSemantics>[ children: <TestSemantics>[
TestSemantics(label: 'included'),
TestSemantics( TestSemantics(
label: 'HELLO', children: <TestSemantics>[
actions: <SemanticsAction>[ TestSemantics(label: 'included'),
SemanticsAction.tap, TestSemantics(
], label: 'HELLO',
flags: <SemanticsFlag>[ actions: <SemanticsAction>[
SemanticsFlag.isLink, SemanticsAction.tap,
],
flags: <SemanticsFlag>[
SemanticsFlag.isLink,
],
),
TestSemantics(label: 'included2'),
], ],
), ),
TestSemantics(label: 'included2'),
], ],
), ),
], ignoreId: true,
), ignoreRect: true,
ignoreId: true, ignoreTransform: true,
ignoreRect: true, ),
ignoreTransform: true, );
)); semantics.dispose();
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/87877 }, skip: isBrowser); // https://github.com/flutter/flutter/issues/87877
// Regression test for https://github.com/flutter/flutter/issues/69787 // Regression test for https://github.com/flutter/flutter/issues/69787
@ -1232,29 +1238,34 @@ void main() {
), ),
); );
expect(semantics, hasSemantics(TestSemantics.root( expect(
children: <TestSemantics>[ semantics,
TestSemantics( hasSemantics(
TestSemantics.root(
children: <TestSemantics>[ children: <TestSemantics>[
TestSemantics(label: 'foo'),
TestSemantics(label: 'bar'),
TestSemantics( TestSemantics(
label: 'HELLO', children: <TestSemantics>[
actions: <SemanticsAction>[ TestSemantics(label: 'foo'),
SemanticsAction.tap, TestSemantics(label: 'bar'),
], TestSemantics(
flags: <SemanticsFlag>[ label: 'HELLO',
SemanticsFlag.isLink, actions: <SemanticsAction>[
SemanticsAction.tap,
],
flags: <SemanticsFlag>[
SemanticsFlag.isLink,
],
),
], ],
), ),
], ],
), ),
], ignoreId: true,
), ignoreRect: true,
ignoreId: true, ignoreTransform: true,
ignoreRect: true, ),
ignoreTransform: true, );
)); semantics.dispose();
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/87877 }, skip: isBrowser); // https://github.com/flutter/flutter/issues/87877
// Regression test for https://github.com/flutter/flutter/issues/69787 // Regression test for https://github.com/flutter/flutter/issues/69787
@ -1303,24 +1314,29 @@ void main() {
), ),
); );
expect(semantics, hasSemantics(TestSemantics.root( expect(
children: <TestSemantics>[ semantics,
TestSemantics( hasSemantics(
TestSemantics.root(
children: <TestSemantics>[ children: <TestSemantics>[
TestSemantics(label: 'not clipped'),
TestSemantics( TestSemantics(
label: 'next WS is clipped', children: <TestSemantics>[
flags: <SemanticsFlag>[SemanticsFlag.isLink], TestSemantics(label: 'not clipped'),
actions: <SemanticsAction>[SemanticsAction.tap], TestSemantics(
label: 'next WS is clipped',
flags: <SemanticsFlag>[SemanticsFlag.isLink],
actions: <SemanticsAction>[SemanticsAction.tap],
),
],
), ),
], ],
), ),
], ignoreId: true,
), ignoreRect: true,
ignoreId: true, ignoreTransform: true,
ignoreRect: true, ),
ignoreTransform: true, );
)); semantics.dispose();
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/87877 }, skip: isBrowser); // https://github.com/flutter/flutter/issues/87877
testWidgets('RenderParagraph intrinsic width', (WidgetTester tester) async { testWidgets('RenderParagraph intrinsic width', (WidgetTester tester) async {

View File

@ -473,7 +473,7 @@ void main() {
}); });
testWidgets('works when semantics are enabled', (WidgetTester tester) async { testWidgets('works when semantics are enabled', (WidgetTester tester) async {
final SemanticsHandle semantics = RendererBinding.instance.pipelineOwner.ensureSemantics(); final SemanticsHandle semantics = tester.ensureSemantics();
await tester.pumpWidget( await tester.pumpWidget(
const Text('hello', textDirection: TextDirection.ltr)); const Text('hello', textDirection: TextDirection.ltr));
@ -497,7 +497,7 @@ void main() {
}, semanticsEnabled: false); }, semanticsEnabled: false);
testWidgets('throws state error multiple matches are found', (WidgetTester tester) async { 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( await tester.pumpWidget(
Directionality( Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,

View File

@ -155,10 +155,10 @@ void testWidgets(
() { () {
tester._testDescription = combinedDescription; tester._testDescription = combinedDescription;
SemanticsHandle? semanticsHandle; SemanticsHandle? semanticsHandle;
tester._recordNumberOfSemanticsHandles();
if (semanticsEnabled == true) { if (semanticsEnabled == true) {
semanticsHandle = tester.ensureSemantics(); semanticsHandle = tester.ensureSemantics();
} }
tester._recordNumberOfSemanticsHandles();
test_package.addTearDown(binding.postTest); test_package.addTearDown(binding.postTest);
return binding.runTest( return binding.runTest(
() async { () async {
@ -1044,18 +1044,16 @@ class WidgetTester extends WidgetController implements HitTestDispatcher, Ticker
void _verifySemanticsHandlesWereDisposed() { void _verifySemanticsHandlesWereDisposed() {
assert(_lastRecordedSemanticsHandles != null); 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>[ throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('A SemanticsHandle was active at the end of the test.'), ErrorSummary('A SemanticsHandle was active at the end of the test.'),
ErrorDescription( ErrorDescription(
'All SemanticsHandle instances must be disposed by calling dispose() on ' 'All SemanticsHandle instances must be disposed by calling dispose() on '
'the SemanticsHandle.' '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; _lastRecordedSemanticsHandles = null;
@ -1063,8 +1061,10 @@ class WidgetTester extends WidgetController implements HitTestDispatcher, Ticker
int? _lastRecordedSemanticsHandles; int? _lastRecordedSemanticsHandles;
int get _currentSemanticsHandles => binding.debugOutstandingSemanticsHandles + binding.pipelineOwner.debugOutstandingSemanticsHandles;
void _recordNumberOfSemanticsHandles() { void _recordNumberOfSemanticsHandles() {
_lastRecordedSemanticsHandles = binding.pipelineOwner.debugOutstandingSemanticsHandles; _lastRecordedSemanticsHandles = _currentSemanticsHandles;
} }
/// Returns the TestTextInput singleton. /// Returns the TestTextInput singleton.

View File

@ -737,7 +737,6 @@ void main() {
testWidgets('failure does not throw unexpected errors', (WidgetTester tester) async { testWidgets('failure does not throw unexpected errors', (WidgetTester tester) async {
final SemanticsHandle handle = tester.ensureSemantics(); final SemanticsHandle handle = tester.ensureSemantics();
addTearDown(() => handle.dispose());
const Key key = Key('semantics'); const Key key = Key('semantics');
await tester.pumpWidget(Semantics( await tester.pumpWidget(Semantics(
@ -789,13 +788,13 @@ void main() {
); );
expect(failedExpectation, throwsA(isA<TestFailure>())); expect(failedExpectation, throwsA(isA<TestFailure>()));
handle.dispose();
}); });
}); });
group('containsSemantics', () { group('containsSemantics', () {
testWidgets('matches SemanticsData', (WidgetTester tester) async { testWidgets('matches SemanticsData', (WidgetTester tester) async {
final SemanticsHandle handle = tester.ensureSemantics(); final SemanticsHandle handle = tester.ensureSemantics();
addTearDown(() => handle.dispose());
const Key key = Key('semantics'); const Key key = Key('semantics');
await tester.pumpWidget(Semantics( await tester.pumpWidget(Semantics(
@ -889,6 +888,7 @@ void main() {
)), )),
reason: 'onTapHint "scans" should not have matched "scan".', reason: 'onTapHint "scans" should not have matched "scan".',
); );
handle.dispose();
}); });
testWidgets('can match all semantics flags and actions enabled', (WidgetTester tester) async { 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 { testWidgets('failure does not throw unexpected errors', (WidgetTester tester) async {
final SemanticsHandle handle = tester.ensureSemantics(); final SemanticsHandle handle = tester.ensureSemantics();
addTearDown(() => handle.dispose());
const Key key = Key('semantics'); const Key key = Key('semantics');
await tester.pumpWidget(Semantics( await tester.pumpWidget(Semantics(
@ -1283,6 +1282,7 @@ void main() {
); );
expect(failedExpectation, throwsA(isA<TestFailure>())); expect(failedExpectation, throwsA(isA<TestFailure>()));
handle.dispose();
}); });
}); });

View File

@ -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>'));
});
}

View File

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

View File

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

View File

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