Unverified Commit 6de42a70 authored by Michael Goderbauer's avatar Michael Goderbauer Committed by GitHub

Fix how tests count open SemanticsHandles (#121571)

Fix how tests count open SemanticsHandles
parent f97a5153
......@@ -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
......
......@@ -1247,6 +1247,7 @@ void main() {
label: 'Custom label',
flags: <SemanticsFlag>[SemanticsFlag.namesRoute],
)));
semantics.dispose();
});
testWidgets('CupertinoDialogRoute is state restorable', (WidgetTester tester) async {
......
......@@ -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 {
......
......@@ -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();
});
});
});
......
......@@ -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();
});
});
......
......@@ -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();
});
});
}
......
......@@ -2505,6 +2505,7 @@ void main() {
label: 'Custom label',
flags: <SemanticsFlag>[SemanticsFlag.namesRoute],
)));
semantics.dispose();
});
testWidgets('DialogRoute is state restorable', (WidgetTester tester) async {
......
......@@ -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 {
......
......@@ -4514,6 +4514,7 @@ void main() {
),
],
), ignoreTransform: true, ignoreRect: true));
semantics.dispose();
});
testWidgets('TextField with specified suffixStyle', (WidgetTester tester) async {
......
......@@ -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();
});
}
......@@ -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(
......
......@@ -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 {
......
......@@ -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
......
......@@ -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();
});
});
......
......@@ -2297,6 +2297,7 @@ void main() {
),
],
), ignoreTransform: true, ignoreRect: true));
semantics.dispose();
});
testWidgets('SelectableText semantics, enableInteractiveSelection = false', (WidgetTester tester) async {
......
......@@ -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 {
......
......@@ -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
......
......@@ -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 {
......
......@@ -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();
});
});
......
......@@ -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 {
......
......@@ -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,
......
......@@ -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.
......
......@@ -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();
});
});
......
// 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>'));
});
}
// 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();
// 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();
// 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();
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment