blob: 89f6d73339101487f9318005b49d8875ec46c725 [file] [log] [blame]
// 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 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:vector_math/vector_math_64.dart';
import '../rendering/rendering_tester.dart';
const int kMaxFrameworkAccessibilityIdentifier = (1<<16) - 1;
void main() {
TestRenderingFlutterBinding.ensureInitialized();
setUp(() {
debugResetSemanticsIdCounter();
});
group('SemanticsNode', () {
const SemanticsTag tag1 = SemanticsTag('Tag One');
const SemanticsTag tag2 = SemanticsTag('Tag Two');
const SemanticsTag tag3 = SemanticsTag('Tag Three');
test('tagging', () {
final SemanticsNode node = SemanticsNode();
expect(node.isTagged(tag1), isFalse);
expect(node.isTagged(tag2), isFalse);
node.tags = <SemanticsTag>{tag1};
expect(node.isTagged(tag1), isTrue);
expect(node.isTagged(tag2), isFalse);
node.tags!.add(tag2);
expect(node.isTagged(tag1), isTrue);
expect(node.isTagged(tag2), isTrue);
});
test('getSemanticsData includes tags', () {
final Set<SemanticsTag> tags = <SemanticsTag>{tag1, tag2};
final SemanticsNode node = SemanticsNode()
..rect = const Rect.fromLTRB(0.0, 0.0, 10.0, 10.0)
..tags = tags;
expect(node.getSemanticsData().tags, tags);
tags.add(tag3);
final SemanticsConfiguration config = SemanticsConfiguration()
..isSemanticBoundary = true
..isMergingSemanticsOfDescendants = true;
node.updateWith(
config: config,
childrenInInversePaintOrder: <SemanticsNode>[
SemanticsNode()
..isMergedIntoParent = true
..rect = const Rect.fromLTRB(5.0, 5.0, 10.0, 10.0)
..tags = tags,
],
);
expect(node.getSemanticsData().tags, tags);
});
test('SemanticsConfiguration can set both string label/value/hint and attributed version', () {
final SemanticsConfiguration config = SemanticsConfiguration();
config.label = 'label1';
expect(config.label, 'label1');
expect(config.attributedLabel.string, 'label1');
expect(config.attributedLabel.attributes.isEmpty, isTrue);
expect(
(SemanticsNode()..updateWith(config: config)).toString(),
'SemanticsNode#1(STALE, owner: null, Rect.fromLTRB(0.0, 0.0, 0.0, 0.0), invisible, label: "label1")',
);
config.attributedLabel = AttributedString(
'label2',
attributes: <StringAttribute>[
SpellOutStringAttribute(range: const TextRange(start: 0, end:1)),
]
);
expect(config.label, 'label2');
expect(config.attributedLabel.string, 'label2');
expect(config.attributedLabel.attributes.length, 1);
expect(config.attributedLabel.attributes[0] is SpellOutStringAttribute, isTrue);
expect(config.attributedLabel.attributes[0].range, const TextRange(start: 0, end: 1));
expect(
(SemanticsNode()..updateWith(config: config)).toString(),
'SemanticsNode#2(STALE, owner: null, Rect.fromLTRB(0.0, 0.0, 0.0, 0.0), invisible, label: "label2" [SpellOutStringAttribute(TextRange(start: 0, end: 1))])',
);
config.label = 'label3';
expect(config.label, 'label3');
expect(config.attributedLabel.string, 'label3');
expect(config.attributedLabel.attributes.isEmpty, isTrue);
expect(
(SemanticsNode()..updateWith(config: config)).toString(),
'SemanticsNode#3(STALE, owner: null, Rect.fromLTRB(0.0, 0.0, 0.0, 0.0), invisible, label: "label3")',
);
config.value = 'value1';
expect(config.value, 'value1');
expect(config.attributedValue.string, 'value1');
expect(config.attributedValue.attributes.isEmpty, isTrue);
expect(
(SemanticsNode()..updateWith(config: config)).toString(),
'SemanticsNode#4(STALE, owner: null, Rect.fromLTRB(0.0, 0.0, 0.0, 0.0), invisible, label: "label3", value: "value1")',
);
config.attributedValue = AttributedString(
'value2',
attributes: <StringAttribute>[
SpellOutStringAttribute(range: const TextRange(start: 0, end:1)),
]
);
expect(config.value, 'value2');
expect(config.attributedValue.string, 'value2');
expect(config.attributedValue.attributes.length, 1);
expect(config.attributedValue.attributes[0] is SpellOutStringAttribute, isTrue);
expect(config.attributedValue.attributes[0].range, const TextRange(start: 0, end: 1));
expect(
(SemanticsNode()..updateWith(config: config)).toString(),
'SemanticsNode#5(STALE, owner: null, Rect.fromLTRB(0.0, 0.0, 0.0, 0.0), invisible, label: "label3", value: "value2" [SpellOutStringAttribute(TextRange(start: 0, end: 1))])',
);
config.value = 'value3';
expect(config.value, 'value3');
expect(config.attributedValue.string, 'value3');
expect(config.attributedValue.attributes.isEmpty, isTrue);
expect(
(SemanticsNode()..updateWith(config: config)).toString(),
'SemanticsNode#6(STALE, owner: null, Rect.fromLTRB(0.0, 0.0, 0.0, 0.0), invisible, label: "label3", value: "value3")',
);
config.hint = 'hint1';
expect(config.hint, 'hint1');
expect(config.attributedHint.string, 'hint1');
expect(config.attributedHint.attributes.isEmpty, isTrue);
expect(
(SemanticsNode()..updateWith(config: config)).toString(),
'SemanticsNode#7(STALE, owner: null, Rect.fromLTRB(0.0, 0.0, 0.0, 0.0), invisible, label: "label3", value: "value3", hint: "hint1")',
);
config.attributedHint = AttributedString(
'hint2',
attributes: <StringAttribute>[
SpellOutStringAttribute(range: const TextRange(start: 0, end:1)),
]
);
expect(config.hint, 'hint2');
expect(config.attributedHint.string, 'hint2');
expect(config.attributedHint.attributes.length, 1);
expect(config.attributedHint.attributes[0] is SpellOutStringAttribute, isTrue);
expect(config.attributedHint.attributes[0].range, const TextRange(start: 0, end: 1));
expect(
(SemanticsNode()..updateWith(config: config)).toString(),
'SemanticsNode#8(STALE, owner: null, Rect.fromLTRB(0.0, 0.0, 0.0, 0.0), invisible, label: "label3", value: "value3", hint: "hint2" [SpellOutStringAttribute(TextRange(start: 0, end: 1))])',
);
config.hint = 'hint3';
expect(config.hint, 'hint3');
expect(config.attributedHint.string, 'hint3');
expect(config.attributedHint.attributes.isEmpty, isTrue);
expect(
(SemanticsNode()..updateWith(config: config)).toString(),
'SemanticsNode#9(STALE, owner: null, Rect.fromLTRB(0.0, 0.0, 0.0, 0.0), invisible, label: "label3", value: "value3", hint: "hint3")',
);
});
test('mutate existing semantic node list errors', () {
final SemanticsNode node = SemanticsNode()
..rect = const Rect.fromLTRB(0.0, 0.0, 10.0, 10.0);
final SemanticsConfiguration config = SemanticsConfiguration()
..isSemanticBoundary = true
..isMergingSemanticsOfDescendants = true;
final List<SemanticsNode> children = <SemanticsNode>[
SemanticsNode()
..isMergedIntoParent = true
..rect = const Rect.fromLTRB(5.0, 5.0, 10.0, 10.0),
];
node.updateWith(
config: config,
childrenInInversePaintOrder: children,
);
children.add(
SemanticsNode()
..isMergedIntoParent = true
..rect = const Rect.fromLTRB(42.0, 42.0, 10.0, 10.0),
);
{
late FlutterError error;
try {
node.updateWith(
config: config,
childrenInInversePaintOrder: children,
);
} on FlutterError catch (e) {
error = e;
}
expect(error.toString(), equalsIgnoringHashCodes(
'Failed to replace child semantics nodes because the list of `SemanticsNode`s was mutated.\n'
'Instead of mutating the existing list, create a new list containing the desired `SemanticsNode`s.\n'
'Error details:\n'
"The list's length has changed from 1 to 2.",
));
expect(
error.diagnostics.singleWhere((DiagnosticsNode node) => node.level == DiagnosticLevel.hint).toString(),
'Instead of mutating the existing list, create a new list containing the desired `SemanticsNode`s.',
);
}
{
late FlutterError error;
final List<SemanticsNode> modifiedChildren = <SemanticsNode>[
SemanticsNode()
..isMergedIntoParent = true
..rect = const Rect.fromLTRB(5.0, 5.0, 10.0, 10.0),
SemanticsNode()
..isMergedIntoParent = true
..rect = const Rect.fromLTRB(10.0, 10.0, 20.0, 20.0),
];
node.updateWith(
config: config,
childrenInInversePaintOrder: modifiedChildren,
);
try {
modifiedChildren[0] = SemanticsNode()
..isMergedIntoParent = true
..rect = const Rect.fromLTRB(0.0, 0.0, 20.0, 20.0);
modifiedChildren[1] = SemanticsNode()
..isMergedIntoParent = true
..rect = const Rect.fromLTRB(40.0, 14.0, 20.0, 20.0);
node.updateWith(
config: config,
childrenInInversePaintOrder: modifiedChildren,
);
} on FlutterError catch (e) {
error = e;
}
expect(error.toStringDeep(), equalsIgnoringHashCodes(
'FlutterError\n'
' Failed to replace child semantics nodes because the list of\n'
' `SemanticsNode`s was mutated.\n'
' Instead of mutating the existing list, create a new list\n'
' containing the desired `SemanticsNode`s.\n'
' Error details:\n'
' Child node at position 0 was replaced:\n'
' Previous child: SemanticsNode#6(STALE, owner: null, merged up ⬆️, Rect.fromLTRB(0.0, 0.0, 20.0, 20.0))\n'
' New child: SemanticsNode#4(STALE, owner: null, merged up ⬆️, Rect.fromLTRB(5.0, 5.0, 10.0, 10.0))\n'
'\n'
' Child node at position 1 was replaced:\n'
' Previous child: SemanticsNode#7(STALE, owner: null, merged up ⬆️, Rect.fromLTRB(40.0, 14.0, 20.0, 20.0))\n'
' New child: SemanticsNode#5(STALE, owner: null, merged up ⬆️, Rect.fromLTRB(10.0, 10.0, 20.0, 20.0))\n',
));
expect(
error.diagnostics.singleWhere((DiagnosticsNode node) => node.level == DiagnosticLevel.hint).toString(),
'Instead of mutating the existing list, create a new list containing the desired `SemanticsNode`s.',
);
// Two previous children and two new children.
expect(error.diagnostics.where((DiagnosticsNode node) => node.value is SemanticsNode).length, 4);
}
});
test('after markNeedsSemanticsUpdate() all render objects between two semantic boundaries are asked for annotations', () {
TestRenderingFlutterBinding.instance.pipelineOwner.ensureSemantics();
TestRender middle;
final TestRender root = TestRender(
hasTapAction: true,
isSemanticBoundary: true,
child: TestRender(
hasLongPressAction: true,
child: middle = TestRender(
hasScrollLeftAction: true,
child: TestRender(
hasScrollRightAction: true,
child: TestRender(
hasScrollUpAction: true,
isSemanticBoundary: true,
),
),
),
),
);
layout(root);
pumpFrame(phase: EnginePhase.flushSemantics);
int expectedActions = SemanticsAction.tap.index | SemanticsAction.longPress.index | SemanticsAction.scrollLeft.index | SemanticsAction.scrollRight.index;
expect(root.debugSemantics!.getSemanticsData().actions, expectedActions);
middle
..hasScrollLeftAction = false
..hasScrollDownAction = true;
middle.markNeedsSemanticsUpdate();
pumpFrame(phase: EnginePhase.flushSemantics);
expectedActions = SemanticsAction.tap.index | SemanticsAction.longPress.index | SemanticsAction.scrollDown.index | SemanticsAction.scrollRight.index;
expect(root.debugSemantics!.getSemanticsData().actions, expectedActions);
});
});
test('toStringDeep() does not throw with transform == null', () {
final SemanticsNode child1 = SemanticsNode()
..rect = const Rect.fromLTRB(0.0, 0.0, 5.0, 5.0);
final SemanticsNode child2 = SemanticsNode()
..rect = const Rect.fromLTRB(5.0, 0.0, 10.0, 5.0);
final SemanticsNode root = SemanticsNode()
..rect = const Rect.fromLTRB(0.0, 0.0, 10.0, 5.0);
root.updateWith(
config: null,
childrenInInversePaintOrder: <SemanticsNode>[child1, child2],
);
expect(root.transform, isNull);
expect(child1.transform, isNull);
expect(child2.transform, isNull);
expect(
root.toStringDeep(),
'SemanticsNode#3\n'
' │ STALE\n'
' │ owner: null\n'
' │ Rect.fromLTRB(0.0, 0.0, 10.0, 5.0)\n'
' │\n'
' ├─SemanticsNode#1\n'
' │ STALE\n'
' │ owner: null\n'
' │ Rect.fromLTRB(0.0, 0.0, 5.0, 5.0)\n'
' │\n'
' └─SemanticsNode#2\n'
' STALE\n'
' owner: null\n'
' Rect.fromLTRB(5.0, 0.0, 10.0, 5.0)\n',
);
});
test('Incompatible OrdinalSortKey throw AssertionError when compared', () {
// Different types.
expect(() {
const OrdinalSortKey(0.0).compareTo(const CustomSortKey(0.0));
}, throwsAssertionError);
});
test('OrdinalSortKey compares correctly when names are the same', () {
const List<List<SemanticsSortKey>> tests = <List<SemanticsSortKey>>[
<SemanticsSortKey>[OrdinalSortKey(0.0), OrdinalSortKey(0.0)],
<SemanticsSortKey>[OrdinalSortKey(0.0), OrdinalSortKey(1.0)],
<SemanticsSortKey>[OrdinalSortKey(1.0), OrdinalSortKey(0.0)],
<SemanticsSortKey>[OrdinalSortKey(1.0), OrdinalSortKey(1.0)],
<SemanticsSortKey>[OrdinalSortKey(0.0, name: 'a'), OrdinalSortKey(0.0, name: 'a')],
<SemanticsSortKey>[OrdinalSortKey(0.0, name: 'a'), OrdinalSortKey(1.0, name: 'a')],
<SemanticsSortKey>[OrdinalSortKey(1.0, name: 'a'), OrdinalSortKey(0.0, name: 'a')],
<SemanticsSortKey>[OrdinalSortKey(1.0, name: 'a'), OrdinalSortKey(1.0, name: 'a')],
];
final List<int> expectedResults = <int>[0, -1, 1, 0, 0, -1, 1, 0];
assert(tests.length == expectedResults.length);
final List<int> results = <int>[
for (final List<SemanticsSortKey> tuple in tests) tuple[0].compareTo(tuple[1]),
];
expect(results, orderedEquals(expectedResults));
// Differing types should throw an assertion.
expect(() => const OrdinalSortKey(0.0).compareTo(const CustomSortKey(0.0)), throwsAssertionError);
});
test('OrdinalSortKey compares correctly when the names are different', () {
const List<List<SemanticsSortKey>> tests = <List<SemanticsSortKey>>[
<SemanticsSortKey>[OrdinalSortKey(0.0), OrdinalSortKey(0.0, name: 'bar')],
<SemanticsSortKey>[OrdinalSortKey(0.0), OrdinalSortKey(1.0, name: 'bar')],
<SemanticsSortKey>[OrdinalSortKey(1.0), OrdinalSortKey(0.0, name: 'bar')],
<SemanticsSortKey>[OrdinalSortKey(1.0), OrdinalSortKey(1.0, name: 'bar')],
<SemanticsSortKey>[OrdinalSortKey(0.0, name: 'foo'), OrdinalSortKey(0.0)],
<SemanticsSortKey>[OrdinalSortKey(0.0, name: 'foo'), OrdinalSortKey(1.0)],
<SemanticsSortKey>[OrdinalSortKey(1.0, name: 'foo'), OrdinalSortKey(0.0)],
<SemanticsSortKey>[OrdinalSortKey(1.0, name: 'foo'), OrdinalSortKey(1.0)],
<SemanticsSortKey>[OrdinalSortKey(0.0, name: 'foo'), OrdinalSortKey(0.0, name: 'bar')],
<SemanticsSortKey>[OrdinalSortKey(0.0, name: 'foo'), OrdinalSortKey(1.0, name: 'bar')],
<SemanticsSortKey>[OrdinalSortKey(1.0, name: 'foo'), OrdinalSortKey(0.0, name: 'bar')],
<SemanticsSortKey>[OrdinalSortKey(1.0, name: 'foo'), OrdinalSortKey(1.0, name: 'bar')],
<SemanticsSortKey>[OrdinalSortKey(0.0, name: 'bar'), OrdinalSortKey(0.0, name: 'foo')],
<SemanticsSortKey>[OrdinalSortKey(0.0, name: 'bar'), OrdinalSortKey(1.0, name: 'foo')],
<SemanticsSortKey>[OrdinalSortKey(1.0, name: 'bar'), OrdinalSortKey(0.0, name: 'foo')],
<SemanticsSortKey>[OrdinalSortKey(1.0, name: 'bar'), OrdinalSortKey(1.0, name: 'foo')],
];
final List<int> expectedResults = <int>[ -1, -1, -1, -1, 1, 1, 1, 1, 1, 1, 1, 1, -1, -1, -1, -1];
assert(tests.length == expectedResults.length);
final List<int> results = <int>[
for (final List<SemanticsSortKey> tuple in tests) tuple[0].compareTo(tuple[1]),
];
expect(results, orderedEquals(expectedResults));
});
test('toStringDeep respects childOrder parameter', () {
final SemanticsNode child1 = SemanticsNode()
..rect = const Rect.fromLTRB(15.0, 0.0, 20.0, 5.0);
final SemanticsNode child2 = SemanticsNode()
..rect = const Rect.fromLTRB(10.0, 0.0, 15.0, 5.0);
final SemanticsNode root = SemanticsNode()
..rect = const Rect.fromLTRB(0.0, 0.0, 20.0, 5.0);
root.updateWith(
config: null,
childrenInInversePaintOrder: <SemanticsNode>[child1, child2],
);
expect(
root.toStringDeep(),
'SemanticsNode#3\n'
' │ STALE\n'
' │ owner: null\n'
' │ Rect.fromLTRB(0.0, 0.0, 20.0, 5.0)\n'
' │\n'
' ├─SemanticsNode#1\n'
' │ STALE\n'
' │ owner: null\n'
' │ Rect.fromLTRB(15.0, 0.0, 20.0, 5.0)\n'
' │\n'
' └─SemanticsNode#2\n'
' STALE\n'
' owner: null\n'
' Rect.fromLTRB(10.0, 0.0, 15.0, 5.0)\n',
);
expect(
root.toStringDeep(childOrder: DebugSemanticsDumpOrder.inverseHitTest),
'SemanticsNode#3\n'
' │ STALE\n'
' │ owner: null\n'
' │ Rect.fromLTRB(0.0, 0.0, 20.0, 5.0)\n'
' │\n'
' ├─SemanticsNode#1\n'
' │ STALE\n'
' │ owner: null\n'
' │ Rect.fromLTRB(15.0, 0.0, 20.0, 5.0)\n'
' │\n'
' └─SemanticsNode#2\n'
' STALE\n'
' owner: null\n'
' Rect.fromLTRB(10.0, 0.0, 15.0, 5.0)\n',
);
final SemanticsNode child3 = SemanticsNode()
..rect = const Rect.fromLTRB(0.0, 0.0, 10.0, 5.0);
child3.updateWith(
config: null,
childrenInInversePaintOrder: <SemanticsNode>[
SemanticsNode()
..rect = const Rect.fromLTRB(5.0, 0.0, 10.0, 5.0),
SemanticsNode()
..rect = const Rect.fromLTRB(0.0, 0.0, 5.0, 5.0),
],
);
final SemanticsNode rootComplex = SemanticsNode()
..rect = const Rect.fromLTRB(0.0, 0.0, 25.0, 5.0);
rootComplex.updateWith(
config: null,
childrenInInversePaintOrder: <SemanticsNode>[child1, child2, child3],
);
expect(
rootComplex.toStringDeep(),
'SemanticsNode#7\n'
' │ STALE\n'
' │ owner: null\n'
' │ Rect.fromLTRB(0.0, 0.0, 25.0, 5.0)\n'
' │\n'
' ├─SemanticsNode#1\n'
' │ STALE\n'
' │ owner: null\n'
' │ Rect.fromLTRB(15.0, 0.0, 20.0, 5.0)\n'
' │\n'
' ├─SemanticsNode#2\n'
' │ STALE\n'
' │ owner: null\n'
' │ Rect.fromLTRB(10.0, 0.0, 15.0, 5.0)\n'
' │\n'
' └─SemanticsNode#4\n'
' │ STALE\n'
' │ owner: null\n'
' │ Rect.fromLTRB(0.0, 0.0, 10.0, 5.0)\n'
' │\n'
' ├─SemanticsNode#5\n'
' │ STALE\n'
' │ owner: null\n'
' │ Rect.fromLTRB(5.0, 0.0, 10.0, 5.0)\n'
' │\n'
' └─SemanticsNode#6\n'
' STALE\n'
' owner: null\n'
' Rect.fromLTRB(0.0, 0.0, 5.0, 5.0)\n',
);
expect(
rootComplex.toStringDeep(childOrder: DebugSemanticsDumpOrder.inverseHitTest),
'SemanticsNode#7\n'
' │ STALE\n'
' │ owner: null\n'
' │ Rect.fromLTRB(0.0, 0.0, 25.0, 5.0)\n'
' │\n'
' ├─SemanticsNode#1\n'
' │ STALE\n'
' │ owner: null\n'
' │ Rect.fromLTRB(15.0, 0.0, 20.0, 5.0)\n'
' │\n'
' ├─SemanticsNode#2\n'
' │ STALE\n'
' │ owner: null\n'
' │ Rect.fromLTRB(10.0, 0.0, 15.0, 5.0)\n'
' │\n'
' └─SemanticsNode#4\n'
' │ STALE\n'
' │ owner: null\n'
' │ Rect.fromLTRB(0.0, 0.0, 10.0, 5.0)\n'
' │\n'
' ├─SemanticsNode#5\n'
' │ STALE\n'
' │ owner: null\n'
' │ Rect.fromLTRB(5.0, 0.0, 10.0, 5.0)\n'
' │\n'
' └─SemanticsNode#6\n'
' STALE\n'
' owner: null\n'
' Rect.fromLTRB(0.0, 0.0, 5.0, 5.0)\n',
);
});
test('debug properties', () {
final SemanticsNode minimalProperties = SemanticsNode();
expect(
minimalProperties.toStringDeep(),
'SemanticsNode#1\n'
' Rect.fromLTRB(0.0, 0.0, 0.0, 0.0)\n'
' invisible\n',
);
expect(
minimalProperties.toStringDeep(minLevel: DiagnosticLevel.hidden),
'SemanticsNode#1\n'
' owner: null\n'
' isMergedIntoParent: false\n'
' mergeAllDescendantsIntoThisNode: false\n'
' Rect.fromLTRB(0.0, 0.0, 0.0, 0.0)\n'
' tags: null\n'
' actions: []\n'
' customActions: []\n'
' flags: []\n'
' invisible\n'
' isHidden: false\n'
' label: ""\n'
' value: ""\n'
' increasedValue: ""\n'
' decreasedValue: ""\n'
' hint: ""\n'
' tooltip: ""\n'
' textDirection: null\n'
' sortKey: null\n'
' platformViewId: null\n'
' maxValueLength: null\n'
' currentValueLength: null\n'
' scrollChildren: null\n'
' scrollIndex: null\n'
' scrollExtentMin: null\n'
' scrollPosition: null\n'
' scrollExtentMax: null\n'
' elevation: 0.0\n'
' thickness: 0.0\n',
);
final SemanticsConfiguration config = SemanticsConfiguration()
..isSemanticBoundary = true
..isMergingSemanticsOfDescendants = true
..onScrollUp = () { }
..onLongPress = () { }
..onShowOnScreen = () { }
..isChecked = false
..isSelected = true
..isButton = true
..label = 'Use all the properties'
..textDirection = TextDirection.rtl
..sortKey = const OrdinalSortKey(1.0);
final SemanticsNode allProperties = SemanticsNode()
..rect = const Rect.fromLTWH(50.0, 10.0, 20.0, 30.0)
..transform = Matrix4.translation(Vector3(10.0, 10.0, 0.0))
..updateWith(config: config);
expect(
allProperties.toStringDeep(),
equalsIgnoringHashCodes(
'SemanticsNode#2\n'
' STALE\n'
' owner: null\n'
' merge boundary ⛔️\n'
' Rect.fromLTRB(60.0, 20.0, 80.0, 50.0)\n'
' actions: longPress, scrollUp, showOnScreen\n'
' flags: hasCheckedState, isSelected, isButton\n'
' label: "Use all the properties"\n'
' textDirection: rtl\n'
' sortKey: OrdinalSortKey#19df5(order: 1.0)\n',
),
);
expect(
allProperties.getSemanticsData().toString(),
'SemanticsData(Rect.fromLTRB(50.0, 10.0, 70.0, 40.0), [1.0,0.0,0.0,10.0; 0.0,1.0,0.0,10.0; 0.0,0.0,1.0,0.0; 0.0,0.0,0.0,1.0], actions: [longPress, scrollUp, showOnScreen], flags: [hasCheckedState, isSelected, isButton], label: "Use all the properties", textDirection: rtl)',
);
final SemanticsNode scaled = SemanticsNode()
..rect = const Rect.fromLTWH(50.0, 10.0, 20.0, 30.0)
..transform = Matrix4.diagonal3(Vector3(10.0, 10.0, 1.0));
expect(
scaled.toStringDeep(),
'SemanticsNode#3\n'
' STALE\n'
' owner: null\n'
' Rect.fromLTRB(50.0, 10.0, 70.0, 40.0) scaled by 10.0x\n',
);
expect(
scaled.getSemanticsData().toString(),
'SemanticsData(Rect.fromLTRB(50.0, 10.0, 70.0, 40.0), [10.0,0.0,0.0,0.0; 0.0,10.0,0.0,0.0; 0.0,0.0,1.0,0.0; 0.0,0.0,0.0,1.0])',
);
});
test('Custom actions debug properties', () {
final SemanticsConfiguration configuration = SemanticsConfiguration();
const CustomSemanticsAction action1 = CustomSemanticsAction(label: 'action1');
const CustomSemanticsAction action2 = CustomSemanticsAction(label: 'action2');
const CustomSemanticsAction action3 = CustomSemanticsAction(label: 'action3');
configuration.customSemanticsActions = <CustomSemanticsAction, VoidCallback>{
action1: () { },
action2: () { },
action3: () { },
};
final SemanticsNode actionNode = SemanticsNode();
actionNode.updateWith(config: configuration);
expect(
actionNode.toStringDeep(minLevel: DiagnosticLevel.hidden),
'SemanticsNode#1\n'
' STALE\n'
' owner: null\n'
' isMergedIntoParent: false\n'
' mergeAllDescendantsIntoThisNode: false\n'
' Rect.fromLTRB(0.0, 0.0, 0.0, 0.0)\n'
' tags: null\n'
' actions: customAction\n'
' customActions: action1, action2, action3\n'
' flags: []\n'
' invisible\n'
' isHidden: false\n'
' label: ""\n'
' value: ""\n'
' increasedValue: ""\n'
' decreasedValue: ""\n'
' hint: ""\n'
' tooltip: ""\n'
' textDirection: null\n'
' sortKey: null\n'
' platformViewId: null\n'
' maxValueLength: null\n'
' currentValueLength: null\n'
' scrollChildren: null\n'
' scrollIndex: null\n'
' scrollExtentMin: null\n'
' scrollPosition: null\n'
' scrollExtentMax: null\n'
' elevation: 0.0\n'
' thickness: 0.0\n',
);
});
test('Attributed String can concate', () {
final AttributedString string1 = AttributedString(
'string1',
attributes: <StringAttribute>[
SpellOutStringAttribute(range: const TextRange(start:0, end:4)),
]
);
final AttributedString string2 = AttributedString(
'string2',
attributes: <StringAttribute>[
LocaleStringAttribute(locale: const Locale('es', 'MX'), range: const TextRange(start:0, end:4)),
]
);
final AttributedString result = string1 + string2;
expect(result.string, 'string1string2');
expect(result.attributes.length, 2);
expect(result.attributes[0].range, const TextRange(start:0, end:4));
expect(result.attributes[0] is SpellOutStringAttribute, isTrue);
expect(result.toString(), "AttributedString('string1string2', attributes: [SpellOutStringAttribute(TextRange(start: 0, end: 4)), LocaleStringAttribute(TextRange(start: 7, end: 11), es-MX)])");
});
test('Semantics id does not repeat', () {
final SemanticsOwner owner = SemanticsOwner();
const int expectId = 1400;
SemanticsNode? nodeToRemove;
for (int i = 0; i < kMaxFrameworkAccessibilityIdentifier; i++) {
final SemanticsNode node = SemanticsNode();
node.attach(owner);
if (node.id == expectId) {
nodeToRemove = node;
}
}
nodeToRemove!.detach();
final SemanticsNode newNode = SemanticsNode();
newNode.attach(owner);
// Id is reused.
expect(newNode.id, expectId);
});
test('Tags show up in debug properties', () {
final SemanticsNode actionNode = SemanticsNode()
..tags = <SemanticsTag>{RenderViewport.useTwoPaneSemantics};
expect(
actionNode.toStringDeep(),
contains('\n tags: RenderViewport.twoPane\n'),
);
});
test('SemanticsConfiguration getter/setter', () {
final SemanticsConfiguration config = SemanticsConfiguration();
const CustomSemanticsAction customAction = CustomSemanticsAction(label: 'test');
expect(config.isSemanticBoundary, isFalse);
expect(config.isButton, isFalse);
expect(config.isLink, isFalse);
expect(config.isMergingSemanticsOfDescendants, isFalse);
expect(config.isEnabled, null);
expect(config.isChecked, null);
expect(config.isSelected, isFalse);
expect(config.isBlockingSemanticsOfPreviouslyPaintedNodes, isFalse);
expect(config.isFocused, isFalse);
expect(config.isTextField, isFalse);
expect(config.onShowOnScreen, isNull);
expect(config.onScrollDown, isNull);
expect(config.onScrollUp, isNull);
expect(config.onScrollLeft, isNull);
expect(config.onScrollRight, isNull);
expect(config.onLongPress, isNull);
expect(config.onDecrease, isNull);
expect(config.onIncrease, isNull);
expect(config.onMoveCursorForwardByCharacter, isNull);
expect(config.onMoveCursorBackwardByCharacter, isNull);
expect(config.onTap, isNull);
expect(config.customSemanticsActions[customAction], isNull);
config.isSemanticBoundary = true;
config.isButton = true;
config.isLink = true;
config.isMergingSemanticsOfDescendants = true;
config.isEnabled = true;
config.isChecked = true;
config.isSelected = true;
config.isBlockingSemanticsOfPreviouslyPaintedNodes = true;
config.isFocused = true;
config.isTextField = true;
void onShowOnScreen() { }
void onScrollDown() { }
void onScrollUp() { }
void onScrollLeft() { }
void onScrollRight() { }
void onLongPress() { }
void onDecrease() { }
void onIncrease() { }
void onMoveCursorForwardByCharacter(bool _) { }
void onMoveCursorBackwardByCharacter(bool _) { }
void onTap() { }
void onCustomAction() { }
config.onShowOnScreen = onShowOnScreen;
config.onScrollDown = onScrollDown;
config.onScrollUp = onScrollUp;
config.onScrollLeft = onScrollLeft;
config.onScrollRight = onScrollRight;
config.onLongPress = onLongPress;
config.onDecrease = onDecrease;
config.onIncrease = onIncrease;
config.onMoveCursorForwardByCharacter = onMoveCursorForwardByCharacter;
config.onMoveCursorBackwardByCharacter = onMoveCursorBackwardByCharacter;
config.onTap = onTap;
config.customSemanticsActions[customAction] = onCustomAction;
expect(config.isSemanticBoundary, isTrue);
expect(config.isButton, isTrue);
expect(config.isLink, isTrue);
expect(config.isMergingSemanticsOfDescendants, isTrue);
expect(config.isEnabled, isTrue);
expect(config.isChecked, isTrue);
expect(config.isSelected, isTrue);
expect(config.isBlockingSemanticsOfPreviouslyPaintedNodes, isTrue);
expect(config.isFocused, isTrue);
expect(config.isTextField, isTrue);
expect(config.onShowOnScreen, same(onShowOnScreen));
expect(config.onScrollDown, same(onScrollDown));
expect(config.onScrollUp, same(onScrollUp));
expect(config.onScrollLeft, same(onScrollLeft));
expect(config.onScrollRight, same(onScrollRight));
expect(config.onLongPress, same(onLongPress));
expect(config.onDecrease, same(onDecrease));
expect(config.onIncrease, same(onIncrease));
expect(config.onMoveCursorForwardByCharacter, same(onMoveCursorForwardByCharacter));
expect(config.onMoveCursorBackwardByCharacter, same(onMoveCursorBackwardByCharacter));
expect(config.onTap, same(onTap));
expect(config.customSemanticsActions[customAction], same(onCustomAction));
});
}
class TestRender extends RenderProxyBox {
TestRender({
this.hasTapAction = false,
this.hasLongPressAction = false,
this.hasScrollLeftAction = false,
this.hasScrollRightAction = false,
this.hasScrollUpAction = false,
this.hasScrollDownAction = false,
this.isSemanticBoundary = false,
RenderBox? child,
}) : super(child);
bool hasTapAction;
bool hasLongPressAction;
bool hasScrollLeftAction;
bool hasScrollRightAction;
bool hasScrollUpAction;
bool hasScrollDownAction;
bool isSemanticBoundary;
@override
void describeSemanticsConfiguration(SemanticsConfiguration config) {
super.describeSemanticsConfiguration(config);
config.isSemanticBoundary = isSemanticBoundary;
if (hasTapAction) {
config.onTap = () { };
}
if (hasLongPressAction) {
config.onLongPress = () { };
}
if (hasScrollLeftAction) {
config.onScrollLeft = () { };
}
if (hasScrollRightAction) {
config.onScrollRight = () { };
}
if (hasScrollUpAction) {
config.onScrollUp = () { };
}
if (hasScrollDownAction) {
config.onScrollDown = () { };
}
}
}
class CustomSortKey extends OrdinalSortKey {
const CustomSortKey(super.order, {super.name});
}