Navaron Bracke 9afbeea427
Cross imports cleanups (#181611)
This PR cleans up a bunch of files for the cross imports, in one big
change, per offline discussion.

The following files have been fixed:

- debug_test.dart
- dual_transition_builder_test.dart
- listener_test.dart
- placeholder_test.dart
- rotated_box_test.dart
- run_app_async_test.dart
- run_app_test.dart
- scroll_activity_test.dart
- scroll_position_test.dart
- scrollable_dispose_test.dart
- semantics_test.dart
- shape_decoration_test.dart
- simple_semantics_test.dart
- sliver_list_test.dart
- slivers_padding_test.dart
- syncing_test.dart
- text_semantics_test.dart

Part of https://github.com/flutter/flutter/issues/177415

*If you had to change anything in the [flutter/tests] repo, include a
link to the migration guide as per the [breaking change policy].*

## Pre-launch Checklist

- [x] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [x] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [x] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [x] I signed the [CLA].
- [x] I listed at least one issue that this PR fixes in the description
above.
- [x] I updated/added relevant documentation (doc comments with `///`).
- [x] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [x] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [x] All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel
on [Discord].

**Note**: The Flutter team is currently trialing the use of [Gemini Code
Assist for
GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code).
Comments from the `gemini-code-assist` bot should not be taken as
authoritative feedback from the Flutter team. If you find its comments
useful you can update your code accordingly, but if you are unsure or
disagree with the feedback, please feel free to wait for a Flutter team
member's review for guidance on which automated comments should be
addressed.

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
2026-02-11 07:33:27 +00:00

2472 lines
75 KiB
Dart

// 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:math';
import 'dart:ui';
import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'semantics_tester.dart';
import 'utils.dart';
void main() {
setUp(() {
debugResetSemanticsIdCounter();
});
testWidgets('Semantics shutdown and restart', (WidgetTester tester) async {
SemanticsTester? semantics = SemanticsTester(tester);
final expectedSemantics = TestSemantics.root(
children: <TestSemantics>[
TestSemantics.rootChild(label: 'test1', textDirection: TextDirection.ltr),
],
);
await tester.pumpWidget(
Semantics(label: 'test1', textDirection: TextDirection.ltr, child: Container()),
);
expect(
semantics,
hasSemantics(expectedSemantics, ignoreTransform: true, ignoreRect: true, ignoreId: true),
);
semantics.dispose();
semantics = null;
expect(tester.binding.hasScheduledFrame, isFalse);
semantics = SemanticsTester(tester);
expect(tester.binding.hasScheduledFrame, isTrue);
await tester.pump();
expect(
semantics,
hasSemantics(expectedSemantics, ignoreTransform: true, ignoreRect: true, ignoreId: true),
);
semantics.dispose();
}, semanticsEnabled: false);
testWidgets('Semantics tag only applies to immediate child', (WidgetTester tester) async {
final semantics = SemanticsTester(tester);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: ListView(
children: <Widget>[
SingleChildScrollView(
child: Column(
children: <Widget>[
Container(padding: const EdgeInsets.only(top: 20.0)),
const Text('label'),
],
),
),
],
),
),
);
expect(
semantics,
isNot(
includesNodeWith(
flags: <SemanticsFlag>[SemanticsFlag.hasImplicitScrolling],
tags: <SemanticsTag>{RenderViewport.useTwoPaneSemantics},
),
),
);
await tester.pump();
// Semantics should stay the same after a frame update.
expect(
semantics,
isNot(
includesNodeWith(
flags: <SemanticsFlag>[SemanticsFlag.hasImplicitScrolling],
tags: <SemanticsTag>{RenderViewport.useTwoPaneSemantics},
),
),
);
semantics.dispose();
}, semanticsEnabled: false);
testWidgets('Semantics tooltip', (WidgetTester tester) async {
final semantics = SemanticsTester(tester);
final expectedSemantics = TestSemantics.root(
children: <TestSemantics>[
TestSemantics.rootChild(tooltip: 'test1', textDirection: TextDirection.ltr),
],
);
await tester.pumpWidget(Semantics(tooltip: 'test1', textDirection: TextDirection.ltr));
expect(
semantics,
hasSemantics(expectedSemantics, ignoreTransform: true, ignoreRect: true, ignoreId: true),
);
semantics.dispose();
});
testWidgets('Detach and reattach assert', (WidgetTester tester) async {
final semantics = SemanticsTester(tester);
final GlobalKey key = GlobalKey();
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Semantics(
label: 'test1',
child: Semantics(key: key, container: true, label: 'test2a', child: Container()),
),
),
);
expect(
semantics,
hasSemantics(
TestSemantics.root(
children: <TestSemantics>[
TestSemantics.rootChild(
label: 'test1',
children: <TestSemantics>[TestSemantics(label: 'test2a')],
),
],
),
ignoreId: true,
ignoreRect: true,
ignoreTransform: true,
),
);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Semantics(
label: 'test1',
child: Semantics(
container: true,
label: 'middle',
child: Semantics(key: key, container: true, label: 'test2b', child: Container()),
),
),
),
);
expect(
semantics,
hasSemantics(
TestSemantics.root(
children: <TestSemantics>[
TestSemantics.rootChild(
label: 'test1',
children: <TestSemantics>[
TestSemantics(
label: 'middle',
children: <TestSemantics>[TestSemantics(label: 'test2b')],
),
],
),
],
),
ignoreId: true,
ignoreRect: true,
ignoreTransform: true,
),
);
semantics.dispose();
});
testWidgets('different locale will not merge', (WidgetTester tester) async {
final semantics = SemanticsTester(tester);
final GlobalKey key = GlobalKey();
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Semantics(
localeForSubtree: const Locale('AB', 'CD'),
child: Semantics(
key: key,
localeForSubtree: const Locale('DE', 'FG'),
child: const SizedBox(width: 10, height: 10),
),
),
),
);
expect(
semantics,
hasSemantics(
TestSemantics.root(
children: <TestSemantics>[
TestSemantics.rootChild(
locale: const Locale('AB', 'CD'),
children: <TestSemantics>[TestSemantics(locale: const Locale('DE', 'FG'))],
),
],
),
ignoreId: true,
ignoreRect: true,
ignoreTransform: true,
),
);
semantics.dispose();
});
testWidgets('Semantics and Directionality - RTL', (WidgetTester tester) async {
final semantics = SemanticsTester(tester);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.rtl,
child: Semantics(label: 'test1', child: Container()),
),
);
expect(semantics, includesNodeWith(label: 'test1', textDirection: TextDirection.rtl));
semantics.dispose();
});
testWidgets('Semantics and Directionality - LTR', (WidgetTester tester) async {
final semantics = SemanticsTester(tester);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Semantics(label: 'test1', child: Container()),
),
);
expect(semantics, includesNodeWith(label: 'test1', textDirection: TextDirection.ltr));
semantics.dispose();
});
testWidgets('Semantics and Directionality - cannot override RTL with LTR', (
WidgetTester tester,
) async {
final semantics = SemanticsTester(tester);
final expectedSemantics = TestSemantics.root(
children: <TestSemantics>[
TestSemantics.rootChild(label: 'test1', textDirection: TextDirection.ltr),
],
);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.rtl,
child: Semantics(label: 'test1', textDirection: TextDirection.ltr, child: Container()),
),
);
expect(
semantics,
hasSemantics(expectedSemantics, ignoreTransform: true, ignoreRect: true, ignoreId: true),
);
semantics.dispose();
});
testWidgets('Semantics and Directionality - cannot override LTR with RTL', (
WidgetTester tester,
) async {
final semantics = SemanticsTester(tester);
final expectedSemantics = TestSemantics.root(
children: <TestSemantics>[
TestSemantics.rootChild(label: 'test1', textDirection: TextDirection.rtl),
],
);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Semantics(label: 'test1', textDirection: TextDirection.rtl, child: Container()),
),
);
expect(
semantics,
hasSemantics(expectedSemantics, ignoreTransform: true, ignoreRect: true, ignoreId: true),
);
semantics.dispose();
});
testWidgets('Semantics label and hint', (WidgetTester tester) async {
final semantics = SemanticsTester(tester);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Semantics(label: 'label', hint: 'hint', value: 'value', child: Container()),
),
);
final expectedSemantics = TestSemantics.root(
children: <TestSemantics>[
TestSemantics.rootChild(
label: 'label',
hint: 'hint',
value: 'value',
textDirection: TextDirection.ltr,
),
],
);
expect(
semantics,
hasSemantics(expectedSemantics, ignoreTransform: true, ignoreRect: true, ignoreId: true),
);
semantics.dispose();
});
testWidgets('Semantics hints can merge', (WidgetTester tester) async {
final semantics = SemanticsTester(tester);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Semantics(
container: true,
child: Column(
children: <Widget>[
Semantics(hint: 'hint one'),
Semantics(hint: 'hint two'),
],
),
),
),
);
final expectedSemantics = TestSemantics.root(
children: <TestSemantics>[
TestSemantics.rootChild(hint: 'hint one\nhint two', textDirection: TextDirection.ltr),
],
);
expect(
semantics,
hasSemantics(expectedSemantics, ignoreTransform: true, ignoreRect: true, ignoreId: true),
);
semantics.dispose();
});
testWidgets('Semantics values do not merge', (WidgetTester tester) async {
final semantics = SemanticsTester(tester);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Semantics(
container: true,
child: Column(
children: <Widget>[
Semantics(value: 'value one', child: const SizedBox(height: 10.0, width: 10.0)),
Semantics(value: 'value two', child: const SizedBox(height: 10.0, width: 10.0)),
],
),
),
),
);
final expectedSemantics = TestSemantics.root(
children: <TestSemantics>[
TestSemantics.rootChild(
children: <TestSemantics>[
TestSemantics(value: 'value one', textDirection: TextDirection.ltr),
TestSemantics(value: 'value two', textDirection: TextDirection.ltr),
],
),
],
);
expect(
semantics,
hasSemantics(expectedSemantics, ignoreTransform: true, ignoreRect: true, ignoreId: true),
);
semantics.dispose();
});
testWidgets('Semantics value and hint can merge', (WidgetTester tester) async {
final semantics = SemanticsTester(tester);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Semantics(
container: true,
child: Column(
children: <Widget>[
Semantics(hint: 'hint'),
Semantics(value: 'value'),
],
),
),
),
);
final expectedSemantics = TestSemantics.root(
children: <TestSemantics>[
TestSemantics.rootChild(hint: 'hint', value: 'value', textDirection: TextDirection.ltr),
],
);
expect(
semantics,
hasSemantics(expectedSemantics, ignoreTransform: true, ignoreRect: true, ignoreId: true),
);
semantics.dispose();
});
testWidgets('Semantics tagForChildren works', (WidgetTester tester) async {
final semantics = SemanticsTester(tester);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Semantics(
container: true,
tagForChildren: const SemanticsTag('custom tag'),
child: Column(
children: <Widget>[
Semantics(container: true, child: const Text('child 1')),
Semantics(container: true, child: const Text('child 2')),
],
),
),
),
);
final expectedSemantics = TestSemantics.root(
children: <TestSemantics>[
TestSemantics.rootChild(
children: <TestSemantics>[
TestSemantics(
label: 'child 1',
tags: <SemanticsTag>[const SemanticsTag('custom tag')],
textDirection: TextDirection.ltr,
),
TestSemantics(
label: 'child 2',
tags: <SemanticsTag>[const SemanticsTag('custom tag')],
textDirection: TextDirection.ltr,
),
],
),
],
);
expect(
semantics,
hasSemantics(expectedSemantics, ignoreTransform: true, ignoreRect: true, ignoreId: true),
);
semantics.dispose();
});
testWidgets('Semantics widget supports all actions', (WidgetTester tester) async {
final semantics = SemanticsTester(tester);
final performedActions = <SemanticsAction>[];
await tester.pumpWidget(
Semantics(
container: true,
onDismiss: () => performedActions.add(SemanticsAction.dismiss),
onTap: () => performedActions.add(SemanticsAction.tap),
onLongPress: () => performedActions.add(SemanticsAction.longPress),
onScrollLeft: () => performedActions.add(SemanticsAction.scrollLeft),
onScrollRight: () => performedActions.add(SemanticsAction.scrollRight),
onScrollUp: () => performedActions.add(SemanticsAction.scrollUp),
onScrollDown: () => performedActions.add(SemanticsAction.scrollDown),
onIncrease: () => performedActions.add(SemanticsAction.increase),
onDecrease: () => performedActions.add(SemanticsAction.decrease),
onCopy: () => performedActions.add(SemanticsAction.copy),
onCut: () => performedActions.add(SemanticsAction.cut),
onPaste: () => performedActions.add(SemanticsAction.paste),
onMoveCursorForwardByCharacter: (bool _) =>
performedActions.add(SemanticsAction.moveCursorForwardByCharacter),
onMoveCursorBackwardByCharacter: (bool _) =>
performedActions.add(SemanticsAction.moveCursorBackwardByCharacter),
onSetSelection: (TextSelection _) => performedActions.add(SemanticsAction.setSelection),
onSetText: (String _) => performedActions.add(SemanticsAction.setText),
onDidGainAccessibilityFocus: () =>
performedActions.add(SemanticsAction.didGainAccessibilityFocus),
onDidLoseAccessibilityFocus: () =>
performedActions.add(SemanticsAction.didLoseAccessibilityFocus),
onFocus: () => performedActions.add(SemanticsAction.focus),
onExpand: () => performedActions.add(SemanticsAction.expand),
onCollapse: () => performedActions.add(SemanticsAction.collapse),
),
);
final Set<SemanticsAction> allActions = SemanticsAction.values.toSet()
..remove(SemanticsAction.moveCursorForwardByWord)
..remove(SemanticsAction.moveCursorBackwardByWord)
..remove(SemanticsAction.customAction) // customAction is not user-exposed.
..remove(SemanticsAction.showOnScreen) // showOnScreen is not user-exposed
..remove(SemanticsAction.scrollToOffset); // scrollToOffset is not user-exposed
const expectedId = 1;
final expectedSemantics = TestSemantics.root(
children: <TestSemantics>[
TestSemantics.rootChild(
id: expectedId,
rect: TestSemantics.fullScreen,
actions: allActions.fold<int>(
0,
(int previous, SemanticsAction action) => previous | action.index,
),
),
],
);
expect(semantics, hasSemantics(expectedSemantics));
// Do the actions work?
final SemanticsOwner semanticsOwner = tester.binding.pipelineOwner.semanticsOwner!;
var expectedLength = 1;
for (final action in allActions) {
switch (action) {
case SemanticsAction.moveCursorBackwardByCharacter:
case SemanticsAction.moveCursorForwardByCharacter:
semanticsOwner.performAction(expectedId, action, true);
case SemanticsAction.setSelection:
semanticsOwner.performAction(expectedId, action, <dynamic, dynamic>{
'base': 4,
'extent': 5,
});
case SemanticsAction.setText:
semanticsOwner.performAction(expectedId, action, 'text');
case SemanticsAction.copy:
case SemanticsAction.customAction:
case SemanticsAction.cut:
case SemanticsAction.decrease:
case SemanticsAction.didGainAccessibilityFocus:
case SemanticsAction.didLoseAccessibilityFocus:
case SemanticsAction.dismiss:
case SemanticsAction.increase:
case SemanticsAction.longPress:
case SemanticsAction.moveCursorBackwardByWord:
case SemanticsAction.moveCursorForwardByWord:
case SemanticsAction.paste:
case SemanticsAction.scrollDown:
case SemanticsAction.scrollLeft:
case SemanticsAction.scrollRight:
case SemanticsAction.scrollUp:
case SemanticsAction.scrollToOffset:
case SemanticsAction.showOnScreen:
case SemanticsAction.tap:
case SemanticsAction.focus:
case SemanticsAction.expand:
case SemanticsAction.collapse:
semanticsOwner.performAction(expectedId, action);
}
expect(performedActions.length, expectedLength);
expect(performedActions.last, action);
expectedLength += 1;
}
semantics.dispose();
});
testWidgets('Semantics widget supports all flags', (WidgetTester tester) async {
final semantics = SemanticsTester(tester);
// Checked state and toggled state are mutually exclusive.
// Focused and isAccessibilityFocusBlocked are mutually exclusive.
await tester.pumpWidget(
Semantics(
key: const Key('a'),
container: true,
explicitChildNodes: true,
// flags
enabled: true,
hidden: true,
checked: true,
selected: true,
button: true,
slider: true,
keyboardKey: true,
link: true,
textField: true,
readOnly: true,
focused: true,
focusable: true,
inMutuallyExclusiveGroup: true,
header: true,
obscured: true,
multiline: true,
scopesRoute: true,
namesRoute: true,
image: true,
liveRegion: true,
expanded: true,
isRequired: true,
),
);
var flags = SemanticsFlags(
isChecked: CheckedState.isTrue,
isSelected: Tristate.isTrue,
isEnabled: Tristate.isTrue,
isExpanded: Tristate.isTrue,
isRequired: Tristate.isTrue,
isFocused: Tristate.isTrue,
isButton: true,
isTextField: true,
isInMutuallyExclusiveGroup: true,
isObscured: true,
scopesRoute: true,
namesRoute: true,
isHidden: true,
isImage: true,
isHeader: true,
isLiveRegion: true,
isMultiline: true,
isReadOnly: true,
isLink: true,
isSlider: true,
isKeyboardKey: true,
);
var expectedSemantics = TestSemantics.root(
children: <TestSemantics>[
TestSemantics.rootChild(rect: TestSemantics.fullScreen, flags: flags),
],
);
expect(semantics, hasSemantics(expectedSemantics, ignoreId: true));
await tester.pumpWidget(Semantics(key: const Key('b'), container: true, scopesRoute: false));
expectedSemantics = TestSemantics.root(
children: <TestSemantics>[
TestSemantics.rootChild(rect: TestSemantics.fullScreen, flags: <SemanticsFlag>[]),
],
);
expect(semantics, hasSemantics(expectedSemantics, ignoreId: true));
await tester.pumpWidget(Semantics(key: const Key('c'), toggled: true));
expectedSemantics = TestSemantics.root(
children: <TestSemantics>[
TestSemantics.rootChild(
rect: TestSemantics.fullScreen,
flags: <SemanticsFlag>[SemanticsFlag.hasToggledState, SemanticsFlag.isToggled],
),
],
);
expect(semantics, hasSemantics(expectedSemantics, ignoreId: true));
await tester.pumpWidget(
Semantics(
key: const Key('a'),
container: true,
explicitChildNodes: true,
// flags
enabled: true,
hidden: true,
checked: false,
mixed: true,
selected: true,
button: true,
slider: true,
keyboardKey: true,
link: true,
textField: true,
readOnly: true,
focused: true,
focusable: true,
inMutuallyExclusiveGroup: true,
header: true,
obscured: true,
multiline: true,
scopesRoute: true,
namesRoute: true,
image: true,
liveRegion: true,
expanded: true,
isRequired: true,
),
);
flags = flags.copyWith(isChecked: CheckedState.mixed);
semantics.dispose();
expectedSemantics = TestSemantics.root(
children: <TestSemantics>[
TestSemantics.rootChild(rect: TestSemantics.fullScreen, flags: flags),
],
);
expect(semantics, hasSemantics(expectedSemantics, ignoreId: true));
});
testWidgets('Actions can be replaced without triggering semantics update', (
WidgetTester tester,
) async {
final semantics = SemanticsTester(tester);
var semanticsUpdateCount = 0;
tester.binding.pipelineOwner.semanticsOwner!.addListener(() {
semanticsUpdateCount += 1;
});
final performedActions = <String>[];
await tester.pumpWidget(Semantics(container: true, onTap: () => performedActions.add('first')));
const expectedId = 1;
final expectedSemantics = TestSemantics.root(
children: <TestSemantics>[
TestSemantics.rootChild(
id: expectedId,
rect: TestSemantics.fullScreen,
actions: SemanticsAction.tap.index,
),
],
);
final SemanticsOwner semanticsOwner = tester.binding.pipelineOwner.semanticsOwner!;
expect(semantics, hasSemantics(expectedSemantics));
semanticsOwner.performAction(expectedId, SemanticsAction.tap);
expect(semanticsUpdateCount, 1);
expect(performedActions, <String>['first']);
semanticsUpdateCount = 0;
performedActions.clear();
// Updating existing handler should not trigger semantics update
await tester.pumpWidget(
Semantics(container: true, onTap: () => performedActions.add('second')),
);
expect(semantics, hasSemantics(expectedSemantics));
semanticsOwner.performAction(expectedId, SemanticsAction.tap);
expect(semanticsUpdateCount, 0);
expect(performedActions, <String>['second']);
semanticsUpdateCount = 0;
performedActions.clear();
// Adding a handler works
await tester.pumpWidget(
Semantics(
container: true,
onTap: () => performedActions.add('second'),
onLongPress: () => performedActions.add('longPress'),
),
);
final expectedSemanticsWithLongPress = TestSemantics.root(
children: <TestSemantics>[
TestSemantics.rootChild(
id: expectedId,
rect: TestSemantics.fullScreen,
actions: SemanticsAction.tap.index | SemanticsAction.longPress.index,
),
],
);
expect(semantics, hasSemantics(expectedSemanticsWithLongPress));
semanticsOwner.performAction(expectedId, SemanticsAction.longPress);
expect(semanticsUpdateCount, 1);
expect(performedActions, <String>['longPress']);
semanticsUpdateCount = 0;
performedActions.clear();
// Removing a handler works
await tester.pumpWidget(
Semantics(container: true, onTap: () => performedActions.add('second')),
);
expect(semantics, hasSemantics(expectedSemantics));
expect(semanticsUpdateCount, 1);
semantics.dispose();
});
testWidgets('onTapHint and onLongPressHint create custom actions', (WidgetTester tester) async {
final SemanticsHandle semantics = tester.ensureSemantics();
await tester.pumpWidget(Semantics(container: true, onTap: () {}, onTapHint: 'test'));
expect(
tester.getSemantics(find.byType(Semantics)),
matchesSemantics(hasTapAction: true, onTapHint: 'test'),
);
await tester.pumpWidget(Semantics(container: true, onLongPress: () {}, onLongPressHint: 'foo'));
expect(
tester.getSemantics(find.byType(Semantics)),
matchesSemantics(hasLongPressAction: true, onLongPressHint: 'foo'),
);
semantics.dispose();
});
testWidgets('CustomSemanticsActions can be added to a Semantics widget', (
WidgetTester tester,
) async {
final SemanticsHandle semantics = tester.ensureSemantics();
await tester.pumpWidget(
Semantics(
container: true,
customSemanticsActions: <CustomSemanticsAction, VoidCallback>{
const CustomSemanticsAction(label: 'foo'): () {},
const CustomSemanticsAction(label: 'bar'): () {},
},
),
);
expect(
tester.getSemantics(find.byType(Semantics)),
matchesSemantics(
customActions: <CustomSemanticsAction>[
const CustomSemanticsAction(label: 'bar'),
const CustomSemanticsAction(label: 'foo'),
],
),
);
semantics.dispose();
});
testWidgets('MergeSemantics merges CustomSemanticsActions from its children', (
WidgetTester tester,
) async {
final SemanticsHandle semantics = tester.ensureSemantics();
await tester.pumpWidget(
MergeSemantics(
child: Column(
children: <Widget>[
Semantics(
container: true,
customSemanticsActions: <CustomSemanticsAction, VoidCallback>{
const CustomSemanticsAction(label: 'action1'): () {},
},
child: const SizedBox(width: 10, height: 10),
),
Semantics(
container: true,
customSemanticsActions: <CustomSemanticsAction, VoidCallback>{
const CustomSemanticsAction(label: 'action2'): () {},
},
child: const SizedBox(width: 10, height: 10),
),
],
),
),
);
expect(
tester.getSemantics(find.byType(MergeSemantics)),
matchesSemantics(
customActions: <CustomSemanticsAction>[
const CustomSemanticsAction(label: 'action1'),
const CustomSemanticsAction(label: 'action2'),
],
),
);
semantics.dispose();
});
testWidgets('AccessiblityFocusBlockType.blockSubtree is applied to the subtree,', (
WidgetTester tester,
) async {
final semantics = SemanticsTester(tester);
await tester.pumpWidget(
Semantics(
container: true,
accessibilityFocusBlockType: AccessibilityFocusBlockType.blockSubtree,
child: Column(
children: <Widget>[
// If the child set blockSubTreeAccessibilityFocus to `none`, it's still blcok because its parent.
Semantics(
container: true,
accessibilityFocusBlockType: AccessibilityFocusBlockType.none,
customSemanticsActions: <CustomSemanticsAction, VoidCallback>{
const CustomSemanticsAction(label: 'action1'): () {},
},
child: const SizedBox(width: 10, height: 10),
),
// If the child doesn't have a value for accessibilityFocusable, it will also use the parent data.
Semantics(
container: true,
customSemanticsActions: <CustomSemanticsAction, VoidCallback>{
const CustomSemanticsAction(label: 'action2'): () {},
},
child: const SizedBox(width: 10, height: 10),
),
],
),
),
);
expect(
semantics,
hasSemantics(
TestSemantics.root(
children: <TestSemantics>[
TestSemantics(
id: 1,
flags: SemanticsFlags(isAccessibilityFocusBlocked: true),
children: <TestSemantics>[
TestSemantics(
id: 2,
flags: SemanticsFlags(isAccessibilityFocusBlocked: true),
actions: <SemanticsAction>[SemanticsAction.customAction],
),
TestSemantics(
id: 3,
flags: SemanticsFlags(isAccessibilityFocusBlocked: true),
actions: <SemanticsAction>[SemanticsAction.customAction],
),
],
),
],
),
ignoreTransform: true,
ignoreRect: true,
ignoreId: true,
),
);
semantics.dispose();
});
testWidgets('AccessiblityFocusBlockType.blockNode is not applied to the subtree,', (
WidgetTester tester,
) async {
final semantics = SemanticsTester(tester);
await tester.pumpWidget(
Semantics(
container: true,
accessibilityFocusBlockType: AccessibilityFocusBlockType.blockNode,
child: Column(
children: <Widget>[
Semantics(
container: true,
accessibilityFocusBlockType: AccessibilityFocusBlockType.none,
customSemanticsActions: <CustomSemanticsAction, VoidCallback>{
const CustomSemanticsAction(label: 'action1'): () {},
},
child: const SizedBox(width: 10, height: 10),
),
Semantics(
container: true,
customSemanticsActions: <CustomSemanticsAction, VoidCallback>{
const CustomSemanticsAction(label: 'action2'): () {},
},
child: const SizedBox(width: 10, height: 10),
),
],
),
),
);
expect(
semantics,
hasSemantics(
TestSemantics.root(
children: <TestSemantics>[
TestSemantics(
id: 1,
flags: SemanticsFlags(isAccessibilityFocusBlocked: true),
children: <TestSemantics>[
TestSemantics(id: 2, actions: <SemanticsAction>[SemanticsAction.customAction]),
TestSemantics(id: 3, actions: <SemanticsAction>[SemanticsAction.customAction]),
],
),
],
),
ignoreTransform: true,
ignoreRect: true,
ignoreId: true,
),
);
semantics.dispose();
});
testWidgets('MergeSemantics merges children with AccessiblityFocusBlockType.blockNode', (
WidgetTester tester,
) async {
final semantics = SemanticsTester(tester);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: MergeSemantics(
child: Column(
children: <Widget>[
Semantics(
container: true,
accessibilityFocusBlockType: AccessibilityFocusBlockType.blockNode,
label: 'node1',
child: const SizedBox(width: 10, height: 10),
),
Semantics(
container: true,
label: 'node2',
child: const SizedBox(width: 10, height: 10),
),
],
),
),
),
);
expect(
semantics,
hasSemantics(
TestSemantics.root(
children: <TestSemantics>[
TestSemantics(
id: 1,
flags: SemanticsFlags(isAccessibilityFocusBlocked: true),
label: 'node1\nnode2',
),
],
),
ignoreTransform: true,
ignoreRect: true,
ignoreId: true,
),
);
semantics.dispose();
});
testWidgets('AccessiblityFocusBlockType.blockNode doesnt merge up or merge down', (
WidgetTester tester,
) async {
final semantics = SemanticsTester(tester);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Semantics(
label: 'root',
child: Semantics(
accessibilityFocusBlockType: AccessibilityFocusBlockType.blockNode,
label: 'semantics label 0',
child: Column(
children: <Widget>[
Semantics(label: 'semantics label 1', child: const SizedBox(width: 10, height: 10)),
Semantics(label: 'semantics label 2', child: const SizedBox(width: 10, height: 10)),
],
),
),
),
),
);
expect(
semantics,
hasSemantics(
TestSemantics.root(
children: <TestSemantics>[
TestSemantics(
id: 1,
label: 'root',
children: <TestSemantics>[
TestSemantics(
id: 2,
label: 'semantics label 0',
flags: SemanticsFlags(isAccessibilityFocusBlocked: true),
children: <TestSemantics>[
TestSemantics(id: 3, label: 'semantics label 1'),
TestSemantics(id: 4, label: 'semantics label 2'),
],
),
],
),
],
),
ignoreTransform: true,
ignoreRect: true,
ignoreId: true,
),
);
semantics.dispose();
});
testWidgets('AccessiblityFocusBlockType.blockSubtree doesnt merge up', (
WidgetTester tester,
) async {
final semantics = SemanticsTester(tester);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Semantics(
label: 'root',
child: Semantics(
accessibilityFocusBlockType: AccessibilityFocusBlockType.blockSubtree,
label: 'semantics label 0',
child: Column(
children: <Widget>[
Semantics(label: 'semantics label 1', child: const SizedBox(width: 10, height: 10)),
Semantics(label: 'semantics label 2', child: const SizedBox(width: 10, height: 10)),
],
),
),
),
),
);
expect(
semantics,
hasSemantics(
TestSemantics.root(
children: <TestSemantics>[
TestSemantics(
id: 1,
label: 'root',
children: <TestSemantics>[
TestSemantics(
id: 2,
label: 'semantics label 0\nsemantics label 1\nsemantics label 2',
flags: SemanticsFlags(isAccessibilityFocusBlocked: true),
),
],
),
],
),
ignoreTransform: true,
ignoreRect: true,
ignoreId: true,
),
);
semantics.dispose();
});
testWidgets('semantics node cant be keyboard focusable but accessibility unfocusable', (
WidgetTester tester,
) async {
await tester.pumpWidget(
Semantics(
container: true,
accessibilityFocusBlockType: AccessibilityFocusBlockType.blockSubtree,
focused: true,
customSemanticsActions: <CustomSemanticsAction, VoidCallback>{
const CustomSemanticsAction(label: 'action1'): () {},
},
child: const SizedBox(width: 10, height: 10),
),
);
final Object? exception = tester.takeException();
expect(exception, isFlutterError);
final error = exception! as FlutterError;
expect(
error.message,
startsWith('A node that is keyboard focusable cannot be set to accessibility unfocusable'),
);
});
testWidgets('Increased/decreased values are annotated', (WidgetTester tester) async {
final semantics = SemanticsTester(tester);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Semantics(
container: true,
value: '10s',
increasedValue: '11s',
decreasedValue: '9s',
onIncrease: () => () {},
onDecrease: () => () {},
),
),
);
expect(
semantics,
hasSemantics(
TestSemantics.root(
children: <TestSemantics>[
TestSemantics.rootChild(
actions: SemanticsAction.increase.index | SemanticsAction.decrease.index,
textDirection: TextDirection.ltr,
value: '10s',
increasedValue: '11s',
decreasedValue: '9s',
),
],
),
ignoreTransform: true,
ignoreRect: true,
ignoreId: true,
),
);
semantics.dispose();
});
testWidgets('Semantics widgets built in a widget tree are sorted properly', (
WidgetTester tester,
) async {
final semantics = SemanticsTester(tester);
var semanticsUpdateCount = 0;
tester.binding.pipelineOwner.semanticsOwner!.addListener(() {
semanticsUpdateCount += 1;
});
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Semantics(
sortKey: const CustomSortKey(0.0),
explicitChildNodes: true,
child: Column(
children: <Widget>[
Semantics(sortKey: const CustomSortKey(3.0), child: const Text('Label 1')),
Semantics(sortKey: const CustomSortKey(2.0), child: const Text('Label 2')),
Semantics(
sortKey: const CustomSortKey(1.0),
explicitChildNodes: true,
child: Row(
children: <Widget>[
Semantics(sortKey: const OrdinalSortKey(3.0), child: const Text('Label 3')),
Semantics(sortKey: const OrdinalSortKey(2.0), child: const Text('Label 4')),
Semantics(sortKey: const OrdinalSortKey(1.0), child: const Text('Label 5')),
],
),
),
],
),
),
),
);
expect(semanticsUpdateCount, 1);
expect(
semantics,
hasSemantics(
TestSemantics.root(
children: <TestSemantics>[
TestSemantics(
id: 1,
children: <TestSemantics>[
TestSemantics(
id: 4,
children: <TestSemantics>[
TestSemantics(id: 7, label: r'Label 5', textDirection: TextDirection.ltr),
TestSemantics(id: 6, label: r'Label 4', textDirection: TextDirection.ltr),
TestSemantics(id: 5, label: r'Label 3', textDirection: TextDirection.ltr),
],
),
TestSemantics(id: 3, label: r'Label 2', textDirection: TextDirection.ltr),
TestSemantics(id: 2, label: r'Label 1', textDirection: TextDirection.ltr),
],
),
],
),
ignoreTransform: true,
ignoreRect: true,
),
);
semantics.dispose();
});
testWidgets('Semantics widgets built with explicit sort orders are sorted properly', (
WidgetTester tester,
) async {
final semantics = SemanticsTester(tester);
var semanticsUpdateCount = 0;
tester.binding.pipelineOwner.semanticsOwner!.addListener(() {
semanticsUpdateCount += 1;
});
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Row(
children: <Widget>[
Semantics(sortKey: const CustomSortKey(3.0), child: const Text('Label 1')),
Semantics(sortKey: const CustomSortKey(1.0), child: const Text('Label 2')),
Semantics(sortKey: const CustomSortKey(2.0), child: const Text('Label 3')),
],
),
),
);
expect(semanticsUpdateCount, 1);
expect(
semantics,
hasSemantics(
TestSemantics.root(
children: <TestSemantics>[
TestSemantics(id: 2, label: r'Label 2', textDirection: TextDirection.ltr),
TestSemantics(id: 3, label: r'Label 3', textDirection: TextDirection.ltr),
TestSemantics(id: 1, label: r'Label 1', textDirection: TextDirection.ltr),
],
),
ignoreTransform: true,
ignoreRect: true,
),
);
semantics.dispose();
});
testWidgets('Semantics widgets without sort orders are sorted properly', (
WidgetTester tester,
) async {
final semantics = SemanticsTester(tester);
var semanticsUpdateCount = 0;
tester.binding.pipelineOwner.semanticsOwner!.addListener(() {
semanticsUpdateCount += 1;
});
await tester.pumpWidget(
const Directionality(
textDirection: TextDirection.ltr,
child: Column(
children: <Widget>[
Text('Label 1'),
Text('Label 2'),
Row(children: <Widget>[Text('Label 3'), Text('Label 4'), Text('Label 5')]),
],
),
),
);
expect(semanticsUpdateCount, 1);
expect(
semantics,
hasSemantics(
TestSemantics(
children: <TestSemantics>[
TestSemantics(label: r'Label 1', textDirection: TextDirection.ltr),
TestSemantics(label: r'Label 2', textDirection: TextDirection.ltr),
TestSemantics(label: r'Label 3', textDirection: TextDirection.ltr),
TestSemantics(label: r'Label 4', textDirection: TextDirection.ltr),
TestSemantics(label: r'Label 5', textDirection: TextDirection.ltr),
],
),
ignoreTransform: true,
ignoreRect: true,
ignoreId: true,
),
);
semantics.dispose();
});
testWidgets('Semantics widgets that are transformed are sorted properly', (
WidgetTester tester,
) async {
final semantics = SemanticsTester(tester);
var semanticsUpdateCount = 0;
tester.binding.pipelineOwner.semanticsOwner!.addListener(() {
semanticsUpdateCount += 1;
});
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Column(
children: <Widget>[
const Text('Label 1'),
const Text('Label 2'),
Transform.rotate(
angle: pi / 2.0,
child: const Row(
children: <Widget>[Text('Label 3'), Text('Label 4'), Text('Label 5')],
),
),
],
),
),
);
expect(semanticsUpdateCount, 1);
expect(
semantics,
hasSemantics(
TestSemantics(
children: <TestSemantics>[
TestSemantics(label: r'Label 1', textDirection: TextDirection.ltr),
TestSemantics(label: r'Label 2', textDirection: TextDirection.ltr),
TestSemantics(label: r'Label 3', textDirection: TextDirection.ltr),
TestSemantics(label: r'Label 4', textDirection: TextDirection.ltr),
TestSemantics(label: r'Label 5', textDirection: TextDirection.ltr),
],
),
ignoreTransform: true,
ignoreRect: true,
ignoreId: true,
),
);
semantics.dispose();
});
testWidgets(
'Semantics widgets without sort orders are sorted properly when no Directionality is present',
(WidgetTester tester) async {
final semantics = SemanticsTester(tester);
var semanticsUpdateCount = 0;
tester.binding.pipelineOwner.semanticsOwner!.addListener(() {
semanticsUpdateCount += 1;
});
await tester.pumpWidget(
Stack(
alignment: Alignment.center,
children: <Widget>[
// Set this up so that the placeholder takes up the whole screen,
// and place the positioned boxes so that if we traverse in the
// geometric order, we would go from box [4, 3, 2, 1, 0], but if we
// go in child order, then we go from box [4, 1, 2, 3, 0]. We're verifying
// that we go in child order here, not geometric order, since there
// is no directionality, so we don't have a geometric opinion about
// horizontal order. We do still want to sort vertically, however,
// which is why the order isn't [0, 1, 2, 3, 4].
Semantics(button: true, child: const Placeholder()),
Positioned(
top: 200.0,
left: 100.0,
child: Semantics(
// Box 0
button: true,
child: const SizedBox(width: 30.0, height: 30.0),
),
),
Positioned(
top: 100.0,
left: 200.0,
child: Semantics(
// Box 1
button: true,
child: const SizedBox(width: 30.0, height: 30.0),
),
),
Positioned(
top: 100.0,
left: 100.0,
child: Semantics(
// Box 2
button: true,
child: const SizedBox(width: 30.0, height: 30.0),
),
),
Positioned(
top: 100.0,
left: 0.0,
child: Semantics(
// Box 3
button: true,
child: const SizedBox(width: 30.0, height: 30.0),
),
),
Positioned(
top: 10.0,
left: 100.0,
child: Semantics(
// Box 4
button: true,
child: const SizedBox(width: 30.0, height: 30.0),
),
),
],
),
);
expect(semanticsUpdateCount, 1);
expect(
semantics,
hasSemantics(
TestSemantics(
children: <TestSemantics>[
TestSemantics(flags: <SemanticsFlag>[SemanticsFlag.isButton]),
TestSemantics(flags: <SemanticsFlag>[SemanticsFlag.isButton]),
TestSemantics(flags: <SemanticsFlag>[SemanticsFlag.isButton]),
TestSemantics(flags: <SemanticsFlag>[SemanticsFlag.isButton]),
TestSemantics(flags: <SemanticsFlag>[SemanticsFlag.isButton]),
TestSemantics(flags: <SemanticsFlag>[SemanticsFlag.isButton]),
],
),
ignoreTransform: true,
ignoreRect: true,
ignoreId: true,
),
);
semantics.dispose();
},
);
testWidgets('Semantics excludeSemantics ignores children', (WidgetTester tester) async {
final semantics = SemanticsTester(tester);
await tester.pumpWidget(
Semantics(
label: 'label',
excludeSemantics: true,
textDirection: TextDirection.ltr,
child: Semantics(label: 'other label', textDirection: TextDirection.ltr),
),
);
expect(
semantics,
hasSemantics(
TestSemantics(
children: <TestSemantics>[
TestSemantics(label: 'label', textDirection: TextDirection.ltr),
],
),
ignoreId: true,
ignoreRect: true,
ignoreTransform: true,
),
);
semantics.dispose();
});
testWidgets('Can change handlers', (WidgetTester tester) async {
await tester.pumpWidget(
Semantics(container: true, label: 'foo', textDirection: TextDirection.ltr),
);
expect(
tester.getSemantics(find.bySemanticsLabel('foo')),
matchesSemantics(label: 'foo', textDirection: TextDirection.ltr),
);
await tester.pumpWidget(
Semantics(container: true, label: 'foo', textDirection: TextDirection.ltr, onTap: () {}),
);
expect(
tester.getSemantics(find.bySemanticsLabel('foo')),
matchesSemantics(label: 'foo', hasTapAction: true, textDirection: TextDirection.ltr),
);
await tester.pumpWidget(
Semantics(container: true, label: 'foo', textDirection: TextDirection.ltr),
);
expect(
tester.getSemantics(find.bySemanticsLabel('foo')),
matchesSemantics(label: 'foo', textDirection: TextDirection.ltr),
);
await tester.pumpWidget(
Semantics(container: true, label: 'foo', textDirection: TextDirection.ltr, onDismiss: () {}),
);
expect(
tester.getSemantics(find.bySemanticsLabel('foo')),
matchesSemantics(label: 'foo', hasDismissAction: true, textDirection: TextDirection.ltr),
);
await tester.pumpWidget(
Semantics(container: true, label: 'foo', textDirection: TextDirection.ltr),
);
expect(
tester.getSemantics(find.bySemanticsLabel('foo')),
matchesSemantics(label: 'foo', textDirection: TextDirection.ltr),
);
await tester.pumpWidget(
Semantics(
container: true,
label: 'foo',
textDirection: TextDirection.ltr,
onLongPress: () {},
),
);
expect(
tester.getSemantics(find.bySemanticsLabel('foo')),
matchesSemantics(label: 'foo', hasLongPressAction: true, textDirection: TextDirection.ltr),
);
await tester.pumpWidget(
Semantics(container: true, label: 'foo', textDirection: TextDirection.ltr),
);
expect(
tester.getSemantics(find.bySemanticsLabel('foo')),
matchesSemantics(label: 'foo', textDirection: TextDirection.ltr),
);
await tester.pumpWidget(
Semantics(
container: true,
label: 'foo',
textDirection: TextDirection.ltr,
onScrollLeft: () {},
),
);
expect(
tester.getSemantics(find.bySemanticsLabel('foo')),
matchesSemantics(label: 'foo', hasScrollLeftAction: true, textDirection: TextDirection.ltr),
);
await tester.pumpWidget(
Semantics(container: true, label: 'foo', textDirection: TextDirection.ltr),
);
expect(
tester.getSemantics(find.bySemanticsLabel('foo')),
matchesSemantics(label: 'foo', textDirection: TextDirection.ltr),
);
await tester.pumpWidget(
Semantics(
container: true,
label: 'foo',
textDirection: TextDirection.ltr,
onScrollRight: () {},
),
);
expect(
tester.getSemantics(find.bySemanticsLabel('foo')),
matchesSemantics(label: 'foo', hasScrollRightAction: true, textDirection: TextDirection.ltr),
);
await tester.pumpWidget(
Semantics(container: true, label: 'foo', textDirection: TextDirection.ltr),
);
expect(
tester.getSemantics(find.bySemanticsLabel('foo')),
matchesSemantics(label: 'foo', textDirection: TextDirection.ltr),
);
await tester.pumpWidget(
Semantics(container: true, label: 'foo', textDirection: TextDirection.ltr, onScrollUp: () {}),
);
expect(
tester.getSemantics(find.bySemanticsLabel('foo')),
matchesSemantics(label: 'foo', hasScrollUpAction: true, textDirection: TextDirection.ltr),
);
await tester.pumpWidget(
Semantics(container: true, label: 'foo', textDirection: TextDirection.ltr),
);
expect(
tester.getSemantics(find.bySemanticsLabel('foo')),
matchesSemantics(label: 'foo', textDirection: TextDirection.ltr),
);
await tester.pumpWidget(
Semantics(
container: true,
label: 'foo',
textDirection: TextDirection.ltr,
onScrollDown: () {},
),
);
expect(
tester.getSemantics(find.bySemanticsLabel('foo')),
matchesSemantics(label: 'foo', hasScrollDownAction: true, textDirection: TextDirection.ltr),
);
await tester.pumpWidget(
Semantics(container: true, label: 'foo', textDirection: TextDirection.ltr),
);
expect(
tester.getSemantics(find.bySemanticsLabel('foo')),
matchesSemantics(label: 'foo', textDirection: TextDirection.ltr),
);
await tester.pumpWidget(
Semantics(container: true, label: 'foo', textDirection: TextDirection.ltr, onIncrease: () {}),
);
expect(
tester.getSemantics(find.bySemanticsLabel('foo')),
matchesSemantics(label: 'foo', hasIncreaseAction: true, textDirection: TextDirection.ltr),
);
await tester.pumpWidget(
Semantics(container: true, label: 'foo', textDirection: TextDirection.ltr),
);
expect(
tester.getSemantics(find.bySemanticsLabel('foo')),
matchesSemantics(label: 'foo', textDirection: TextDirection.ltr),
);
await tester.pumpWidget(
Semantics(container: true, label: 'foo', textDirection: TextDirection.ltr, onDecrease: () {}),
);
expect(
tester.getSemantics(find.bySemanticsLabel('foo')),
matchesSemantics(label: 'foo', hasDecreaseAction: true, textDirection: TextDirection.ltr),
);
await tester.pumpWidget(
Semantics(container: true, label: 'foo', textDirection: TextDirection.ltr),
);
expect(
tester.getSemantics(find.bySemanticsLabel('foo')),
matchesSemantics(label: 'foo', textDirection: TextDirection.ltr),
);
await tester.pumpWidget(
Semantics(container: true, label: 'foo', textDirection: TextDirection.ltr, onCopy: () {}),
);
expect(
tester.getSemantics(find.bySemanticsLabel('foo')),
matchesSemantics(label: 'foo', hasCopyAction: true, textDirection: TextDirection.ltr),
);
await tester.pumpWidget(
Semantics(container: true, label: 'foo', textDirection: TextDirection.ltr),
);
expect(
tester.getSemantics(find.bySemanticsLabel('foo')),
matchesSemantics(label: 'foo', textDirection: TextDirection.ltr),
);
await tester.pumpWidget(
Semantics(container: true, label: 'foo', textDirection: TextDirection.ltr, onCut: () {}),
);
expect(
tester.getSemantics(find.bySemanticsLabel('foo')),
matchesSemantics(label: 'foo', hasCutAction: true, textDirection: TextDirection.ltr),
);
await tester.pumpWidget(
Semantics(container: true, label: 'foo', textDirection: TextDirection.ltr),
);
expect(
tester.getSemantics(find.bySemanticsLabel('foo')),
matchesSemantics(label: 'foo', textDirection: TextDirection.ltr),
);
await tester.pumpWidget(
Semantics(container: true, label: 'foo', textDirection: TextDirection.ltr, onPaste: () {}),
);
expect(
tester.getSemantics(find.bySemanticsLabel('foo')),
matchesSemantics(label: 'foo', hasPasteAction: true, textDirection: TextDirection.ltr),
);
await tester.pumpWidget(
Semantics(container: true, label: 'foo', textDirection: TextDirection.ltr),
);
expect(
tester.getSemantics(find.bySemanticsLabel('foo')),
matchesSemantics(label: 'foo', textDirection: TextDirection.ltr),
);
await tester.pumpWidget(
Semantics(
container: true,
label: 'foo',
textDirection: TextDirection.ltr,
onSetSelection: (TextSelection _) {},
),
);
expect(
tester.getSemantics(find.bySemanticsLabel('foo')),
matchesSemantics(label: 'foo', hasSetSelectionAction: true, textDirection: TextDirection.ltr),
);
await tester.pumpWidget(
Semantics(container: true, label: 'foo', textDirection: TextDirection.ltr),
);
expect(
tester.getSemantics(find.bySemanticsLabel('foo')),
matchesSemantics(label: 'foo', textDirection: TextDirection.ltr),
);
await tester.pumpWidget(
Semantics(
container: true,
label: 'foo',
textDirection: TextDirection.ltr,
onDidGainAccessibilityFocus: () {},
),
);
expect(
tester.getSemantics(find.bySemanticsLabel('foo')),
matchesSemantics(
label: 'foo',
hasDidGainAccessibilityFocusAction: true,
textDirection: TextDirection.ltr,
),
);
await tester.pumpWidget(
Semantics(container: true, label: 'foo', textDirection: TextDirection.ltr),
);
expect(
tester.getSemantics(find.bySemanticsLabel('foo')),
matchesSemantics(label: 'foo', textDirection: TextDirection.ltr),
);
await tester.pumpWidget(
Semantics(
container: true,
label: 'foo',
textDirection: TextDirection.ltr,
onDidLoseAccessibilityFocus: () {},
),
);
expect(
tester.getSemantics(find.bySemanticsLabel('foo')),
matchesSemantics(
label: 'foo',
hasDidLoseAccessibilityFocusAction: true,
textDirection: TextDirection.ltr,
),
);
await tester.pumpWidget(
Semantics(container: true, label: 'foo', textDirection: TextDirection.ltr),
);
expect(
tester.getSemantics(find.bySemanticsLabel('foo')),
matchesSemantics(label: 'foo', textDirection: TextDirection.ltr),
);
});
testWidgets('Semantics with zero transform gets dropped', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/110671.
// Construct a widget tree that will end up with a fitted box that applies
// a zero transform because it does not actually draw its children.
// Assert that this subtree gets dropped (the root node has no children).
await tester.pumpWidget(
const Column(
children: <Widget>[
SizedBox(
height: 0,
width: 500,
child: FittedBox(
child: SizedBox(
height: 55,
width: 266,
child: SingleChildScrollView(child: Column()),
),
),
),
],
),
);
final SemanticsNode node = RendererBinding.instance.renderView.debugSemantics!;
expect(node.transform, null); // Make sure the zero transform didn't end up on the root somehow.
expect(node.childrenCount, 0);
});
testWidgets('blocking user interaction works on explicit child node.', (
WidgetTester tester,
) async {
final key1 = UniqueKey();
final key2 = UniqueKey();
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Semantics(
blockUserActions: true,
explicitChildNodes: true,
child: Column(
children: <Widget>[
Semantics(
key: key1,
label: 'label1',
onTap: () {},
child: const SizedBox(width: 10, height: 10),
),
Semantics(
key: key2,
label: 'label2',
onTap: () {},
child: const SizedBox(width: 10, height: 10),
),
],
),
),
),
);
expect(
tester.getSemantics(find.byKey(key1)),
// Tap action is blocked.
matchesSemantics(label: 'label1'),
);
expect(
tester.getSemantics(find.byKey(key2)),
// Tap action is blocked.
matchesSemantics(label: 'label2'),
);
});
testWidgets('blocking user interaction on a merged child', (WidgetTester tester) async {
final key = UniqueKey();
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Semantics(
key: key,
container: true,
child: Column(
children: <Widget>[
Semantics(
blockUserActions: true,
label: 'label1',
onTap: () {},
child: const SizedBox(width: 10, height: 10),
),
Semantics(
label: 'label2',
onLongPress: () {},
child: const SizedBox(width: 10, height: 10),
),
],
),
),
),
);
expect(
tester.getSemantics(find.byKey(key)),
// Tap action in label1 is blocked,
matchesSemantics(label: 'label1\nlabel2', hasLongPressAction: true),
);
});
testWidgets('does not merge conflicting actions even if one of them is blocked', (
WidgetTester tester,
) async {
final key = UniqueKey();
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Semantics(
key: key,
container: true,
child: Column(
children: <Widget>[
Semantics(
blockUserActions: true,
label: 'label1',
onTap: () {},
child: const SizedBox(width: 10, height: 10),
),
Semantics(
label: 'label2',
onTap: () {},
child: const SizedBox(width: 10, height: 10),
),
],
),
),
),
);
final SemanticsNode node = tester.getSemantics(find.byKey(key));
expect(
node,
matchesSemantics(
children: <Matcher>[
isSemantics(label: 'label1'),
isSemantics(label: 'label2'),
],
),
);
});
testWidgets('supports heading levels', (WidgetTester tester) async {
// Default: not a heading.
expect(Semantics(child: const Text('dummy text')).properties.headingLevel, isNull);
// Headings level 1-6.
for (var level = 1; level <= 6; level++) {
final semantics = Semantics(headingLevel: level, child: const Text('dummy text'));
expect(semantics.properties.headingLevel, level);
}
// Invalid heading levels.
for (final badLevel in const <int>[-1, 0, 7, 8, 9]) {
expect(
() => Semantics(headingLevel: badLevel, child: const Text('dummy text')),
throwsAssertionError,
);
}
});
testWidgets('parent heading level takes precedence when it absorbs a child', (
WidgetTester tester,
) async {
final semantics = SemanticsTester(tester);
Future<SemanticsConfiguration> pumpHeading(int? level) async {
final key = ValueKey<String>('heading-$level');
await tester.pumpWidget(
Semantics(
key: key,
headingLevel: level,
child: Text('Heading level $level', textDirection: TextDirection.ltr),
),
);
final RenderSemanticsAnnotations object = tester.renderObject<RenderSemanticsAnnotations>(
find.byKey(key),
);
final config = SemanticsConfiguration();
object.describeSemanticsConfiguration(config);
return config;
}
// Tuples contain (parent level, child level, expected combined level).
final scenarios = <(int, int, int)>[
// Case: neither are headings
(0, 0, 0), // expect not a heading
// Case: parent not a heading, child always wins.
(0, 1, 1),
(0, 2, 2),
// Case: child not a heading, parent always wins.
(1, 0, 1),
(2, 0, 2),
// Case: child heading level higher, parent still wins.
(3, 2, 3),
(4, 1, 4),
// Case: parent heading level higher, parent still wins.
(2, 3, 2),
(1, 5, 1),
];
for (final scenario in scenarios) {
final int parentLevel = scenario.$1;
final int childLevel = scenario.$2;
final int resultLevel = scenario.$3;
final SemanticsConfiguration parent = await pumpHeading(
parentLevel == 0 ? null : parentLevel,
);
final child = SemanticsConfiguration()..headingLevel = childLevel;
parent.absorb(child);
expect(
reason:
'parent heading level is $parentLevel, '
'child heading level is $childLevel, '
'expecting $resultLevel.',
parent.headingLevel,
resultLevel,
);
}
semantics.dispose();
});
testWidgets('applies heading semantics to semantics tree', (WidgetTester tester) async {
final semantics = SemanticsTester(tester);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: ListView(
children: <Widget>[
for (int level = 1; level <= 6; level++)
Semantics(
key: ValueKey<String>('heading-$level'),
headingLevel: level,
child: Text('Heading level $level'),
),
const Text('This is not a heading'),
],
),
),
);
for (var level = 1; level <= 6; level++) {
final key = ValueKey<String>('heading-$level');
final SemanticsNode node = tester.getSemantics(find.byKey(key));
expect('$node', contains('headingLevel: $level'));
}
final SemanticsNode notHeading = tester.getSemantics(find.text('This is not a heading'));
expect(notHeading, isNot(contains('headingLevel')));
semantics.dispose();
});
testWidgets('RenderSemanticsAnnotations provides validation result', (WidgetTester tester) async {
final semantics = SemanticsTester(tester);
Future<SemanticsConfiguration> pumpValidationResult(SemanticsValidationResult result) async {
final key = ValueKey<String>('validation-$result');
await tester.pumpWidget(
Semantics(
key: key,
validationResult: result,
child: Text('Validation result $result', textDirection: TextDirection.ltr),
),
);
final RenderSemanticsAnnotations object = tester.renderObject<RenderSemanticsAnnotations>(
find.byKey(key),
);
final config = SemanticsConfiguration();
object.describeSemanticsConfiguration(config);
return config;
}
final SemanticsConfiguration noneResult = await pumpValidationResult(
SemanticsValidationResult.none,
);
expect(noneResult.validationResult, SemanticsValidationResult.none);
final SemanticsConfiguration validResult = await pumpValidationResult(
SemanticsValidationResult.valid,
);
expect(validResult.validationResult, SemanticsValidationResult.valid);
final SemanticsConfiguration invalidResult = await pumpValidationResult(
SemanticsValidationResult.invalid,
);
expect(invalidResult.validationResult, SemanticsValidationResult.invalid);
semantics.dispose();
});
testWidgets('validation result precedence', (WidgetTester tester) async {
final semantics = SemanticsTester(tester);
Future<void> expectValidationResult({
required SemanticsValidationResult outer,
required SemanticsValidationResult inner,
required SemanticsValidationResult expected,
}) async {
const key = ValueKey<String>('validated-widget');
await tester.pumpWidget(
Semantics(
validationResult: outer,
child: Semantics(
validationResult: inner,
child: Text(
key: key,
'Outer = $outer; inner = $inner',
textDirection: TextDirection.ltr,
),
),
),
);
final SemanticsNode result = tester.getSemantics(find.byKey(key));
expect(
result,
isSemantics(label: 'Outer = $outer; inner = $inner', validationResult: expected),
);
}
// Outer is none
await expectValidationResult(
outer: SemanticsValidationResult.none,
inner: SemanticsValidationResult.none,
expected: SemanticsValidationResult.none,
);
await expectValidationResult(
outer: SemanticsValidationResult.none,
inner: SemanticsValidationResult.valid,
expected: SemanticsValidationResult.valid,
);
await expectValidationResult(
outer: SemanticsValidationResult.none,
inner: SemanticsValidationResult.invalid,
expected: SemanticsValidationResult.invalid,
);
// Outer is valid
await expectValidationResult(
outer: SemanticsValidationResult.valid,
inner: SemanticsValidationResult.none,
expected: SemanticsValidationResult.valid,
);
await expectValidationResult(
outer: SemanticsValidationResult.valid,
inner: SemanticsValidationResult.valid,
expected: SemanticsValidationResult.valid,
);
await expectValidationResult(
outer: SemanticsValidationResult.valid,
inner: SemanticsValidationResult.invalid,
expected: SemanticsValidationResult.invalid,
);
// Outer is invalid
await expectValidationResult(
outer: SemanticsValidationResult.invalid,
inner: SemanticsValidationResult.none,
expected: SemanticsValidationResult.invalid,
);
await expectValidationResult(
outer: SemanticsValidationResult.invalid,
inner: SemanticsValidationResult.valid,
expected: SemanticsValidationResult.invalid,
);
await expectValidationResult(
outer: SemanticsValidationResult.invalid,
inner: SemanticsValidationResult.invalid,
expected: SemanticsValidationResult.invalid,
);
semantics.dispose();
});
testWidgets('semantics grafting in traversal order', (WidgetTester tester) async {
final semantics = SemanticsTester(tester);
const identifier = '111';
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Column(
children: <Widget>[
Semantics(
traversalParentIdentifier: identifier,
child: const SizedBox.square(dimension: 10),
),
Semantics(
traversalChildIdentifier: identifier,
child: const SizedBox.square(dimension: 10),
),
const SizedBox.square(dimension: 10),
],
),
),
);
// Semantics tree in traversal order. On web, no grafting.
expect(
semantics,
kIsWeb
? hasSemantics(
TestSemantics.root(
children: <TestSemantics>[
TestSemantics(id: 1, traversalParentIdentifier: identifier),
TestSemantics(id: 2, traversalChildIdentifier: identifier),
],
),
ignoreRect: true,
ignoreTransform: true,
)
: hasSemantics(
TestSemantics.root(
children: <TestSemantics>[
TestSemantics.rootChild(
id: 1,
traversalParentIdentifier: identifier,
children: <TestSemantics>[
TestSemantics(id: 2, traversalChildIdentifier: identifier),
],
),
],
),
ignoreRect: true,
ignoreTransform: true,
),
);
// Semantics tree in hit-test order.
expect(
semantics,
hasSemantics(
TestSemantics.root(
children: <TestSemantics>[
TestSemantics(id: 1, traversalParentIdentifier: identifier),
TestSemantics(id: 2, traversalChildIdentifier: identifier),
],
),
ignoreRect: true,
ignoreTransform: true,
childOrder: DebugSemanticsDumpOrder.inverseHitTest,
),
);
semantics.dispose();
});
testWidgets('semantics grafting in traversal order with multiple same traversalChildIdentifier', (
WidgetTester tester,
) async {
final semantics = SemanticsTester(tester);
const identifier = '111';
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Column(
children: <Widget>[
Semantics(
traversalParentIdentifier: identifier,
child: const SizedBox.square(dimension: 10),
),
Semantics(
traversalChildIdentifier: identifier,
child: const SizedBox.square(dimension: 10),
),
Semantics(
traversalChildIdentifier: identifier,
child: const SizedBox.square(dimension: 10),
),
const SizedBox.square(dimension: 10),
],
),
),
);
// Semantics tree in traversal order. On web, no grafting.
expect(
semantics,
kIsWeb
? hasSemantics(
TestSemantics.root(
children: <TestSemantics>[
TestSemantics(id: 1, traversalParentIdentifier: identifier),
TestSemantics(id: 2, traversalChildIdentifier: identifier),
TestSemantics(id: 3, traversalChildIdentifier: identifier),
],
),
ignoreRect: true,
ignoreTransform: true,
)
: hasSemantics(
TestSemantics.root(
children: <TestSemantics>[
TestSemantics.rootChild(
id: 1,
traversalParentIdentifier: identifier,
children: <TestSemantics>[
TestSemantics(id: 2, traversalChildIdentifier: identifier),
TestSemantics(id: 3, traversalChildIdentifier: identifier),
],
),
],
),
ignoreRect: true,
ignoreTransform: true,
),
);
// Semantics tree in hit-test order.
expect(
semantics,
hasSemantics(
TestSemantics.root(
children: <TestSemantics>[
TestSemantics(id: 1, traversalParentIdentifier: identifier),
TestSemantics(id: 2, traversalChildIdentifier: identifier),
TestSemantics(id: 3, traversalChildIdentifier: identifier),
],
),
ignoreRect: true,
ignoreTransform: true,
childOrder: DebugSemanticsDumpOrder.inverseHitTest,
),
);
semantics.dispose();
});
testWidgets(
'no grafting in traversal order when traversal child is the parent of its traversal parent',
(WidgetTester tester) async {
final semantics = SemanticsTester(tester);
const identifier = '111';
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Semantics(
container: true,
child: Column(
children: <Widget>[
Semantics(
label: 'child_node',
traversalChildIdentifier: identifier,
child: TestButton(
onPressed: () {},
child: Semantics(
label: 'parent_node',
traversalParentIdentifier: identifier,
child: const Text('Button Text'),
),
),
),
const SizedBox.square(dimension: 10),
],
),
),
),
);
final Object? exception = tester.takeException();
expect(exception, isFlutterError);
final error = exception! as FlutterError;
expect(
error.message,
matches(
RegExp(
r'The traversalParent \d+ cannot be the child of the traversalChild \d+ in hit-test order',
),
),
);
semantics.dispose();
},
skip: kIsWeb, // [intended] the web traversal order by using ARIA-OWNS.
);
// Regression test for https://github.com/flutter/flutter/issues/173097.
testWidgets('Semantics geometry can scroll to reveal hidden children in a scrollable container', (
WidgetTester tester,
) async {
final scrollController = ScrollController();
addTearDown(scrollController.dispose);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: SingleChildScrollView(
controller: scrollController,
child: Semantics(
container: true,
explicitChildNodes: true,
child: Column(
children: <Widget>[
for (int i = 0; i < 20; i++) ...<Widget>[
Text('Label text $i'),
const SizedBox(height: 50),
],
],
),
),
),
),
);
final SemanticsNode label10 = tester.getSemantics(
find.text('Label text 10', skipOffstage: false),
);
expect(label10, matchesSemantics(isHidden: true));
scrollController.jumpTo(500);
await tester.pumpAndSettle();
expect(label10, matchesSemantics(isHidden: false)); // ignore: avoid_redundant_argument_values
});
}
class CustomSortKey extends OrdinalSortKey {
const CustomSortKey(super.order, {super.name});
}