
<!-- Thanks for filing a pull request! Reviewers are typically assigned within a week of filing a request. To learn more about code review, see our documentation on Tree Hygiene: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md --> adding a new property in semantics properties called controlsVisibilityOfNodes, where developer can assign SemanticsProperties.identifier of other nodes to indicates which nodes' visibilities this node controls fixes https://github.com/flutter/flutter/issues/162125 ## Pre-launch Checklist - [ ] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [ ] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [ ] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [ ] I signed the [CLA]. - [ ] I listed at least one issue that this PR fixes in the description above. - [ ] I updated/added relevant documentation (doc comments with `///`). - [ ] I added new tests to check the change I am making, or this PR is [test-exempt]. - [ ] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [ ] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. <!-- 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
274 lines
9.8 KiB
Dart
274 lines
9.8 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:typed_data';
|
|
import 'dart:ui' as ui;
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/semantics.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
|
|
void main() {
|
|
SemanticsUpdateTestBinding();
|
|
|
|
testWidgets('Semantics update does not send update for merged nodes.', (
|
|
WidgetTester tester,
|
|
) async {
|
|
final SemanticsHandle handle = tester.ensureSemantics();
|
|
// Pumps a placeholder to trigger the warm up frame.
|
|
await tester.pumpWidget(
|
|
const Placeholder(),
|
|
// Stops right after the warm up frame.
|
|
phase: EnginePhase.build,
|
|
);
|
|
// The warm up frame will send update for an empty semantics tree. We
|
|
// ignore this one time update.
|
|
SemanticsUpdateBuilderSpy.observations.clear();
|
|
|
|
// Builds the real widget tree.
|
|
await tester.pumpWidget(
|
|
Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: MergeSemantics(
|
|
child: Semantics(
|
|
label: 'outer',
|
|
// This semantics node should not be part of the semantics update
|
|
// because it is under another semantics container.
|
|
child: Semantics(label: 'inner', container: true, child: const Text('text')),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(SemanticsUpdateBuilderSpy.observations.length, 2);
|
|
|
|
expect(SemanticsUpdateBuilderSpy.observations.containsKey(0), isTrue);
|
|
expect(SemanticsUpdateBuilderSpy.observations[0]!.childrenInTraversalOrder.length, 1);
|
|
expect(SemanticsUpdateBuilderSpy.observations[0]!.childrenInTraversalOrder[0], 1);
|
|
|
|
expect(SemanticsUpdateBuilderSpy.observations.containsKey(1), isTrue);
|
|
expect(SemanticsUpdateBuilderSpy.observations[1]!.childrenInTraversalOrder.length, 0);
|
|
expect(SemanticsUpdateBuilderSpy.observations[1]!.label, 'outer\ninner\ntext');
|
|
|
|
SemanticsUpdateBuilderSpy.observations.clear();
|
|
|
|
// Updates the inner semantics label and verifies it only sends update for
|
|
// the merged parent.
|
|
await tester.pumpWidget(
|
|
Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: MergeSemantics(
|
|
child: Semantics(
|
|
label: 'outer',
|
|
// This semantics node should not be part of the semantics update
|
|
// because it is under another semantics container.
|
|
child: Semantics(label: 'inner-updated', container: true, child: const Text('text')),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
expect(SemanticsUpdateBuilderSpy.observations.length, 1);
|
|
|
|
expect(SemanticsUpdateBuilderSpy.observations.containsKey(1), isTrue);
|
|
expect(SemanticsUpdateBuilderSpy.observations[1]!.childrenInTraversalOrder.length, 0);
|
|
expect(SemanticsUpdateBuilderSpy.observations[1]!.label, 'outer\ninner-updated\ntext');
|
|
|
|
SemanticsUpdateBuilderSpy.observations.clear();
|
|
handle.dispose();
|
|
}, skip: true); // https://github.com/flutter/flutter/issues/97894
|
|
|
|
testWidgets('Semantics update receives attributed text', (WidgetTester tester) async {
|
|
final SemanticsHandle handle = tester.ensureSemantics();
|
|
// Pumps a placeholder to trigger the warm up frame.
|
|
await tester.pumpWidget(
|
|
const Placeholder(),
|
|
// Stops right after the warm up frame.
|
|
phase: EnginePhase.build,
|
|
);
|
|
// The warm up frame will send update for an empty semantics tree. We
|
|
// ignore this one time update.
|
|
SemanticsUpdateBuilderSpy.observations.clear();
|
|
|
|
// Builds the real widget tree.
|
|
await tester.pumpWidget(
|
|
Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: Semantics(
|
|
attributedLabel: AttributedString(
|
|
'label',
|
|
attributes: <StringAttribute>[
|
|
SpellOutStringAttribute(range: const TextRange(start: 0, end: 5)),
|
|
],
|
|
),
|
|
attributedValue: AttributedString(
|
|
'value',
|
|
attributes: <StringAttribute>[
|
|
LocaleStringAttribute(
|
|
range: const TextRange(start: 0, end: 5),
|
|
locale: const Locale('en', 'MX'),
|
|
),
|
|
],
|
|
),
|
|
attributedHint: AttributedString(
|
|
'hint',
|
|
attributes: <StringAttribute>[
|
|
SpellOutStringAttribute(range: const TextRange(start: 1, end: 2)),
|
|
],
|
|
),
|
|
child: const Placeholder(),
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(SemanticsUpdateBuilderSpy.observations.length, 2);
|
|
|
|
expect(SemanticsUpdateBuilderSpy.observations.containsKey(0), isTrue);
|
|
expect(SemanticsUpdateBuilderSpy.observations[0]!.childrenInTraversalOrder.length, 1);
|
|
expect(SemanticsUpdateBuilderSpy.observations[0]!.childrenInTraversalOrder[0], 1);
|
|
|
|
expect(SemanticsUpdateBuilderSpy.observations.containsKey(1), isTrue);
|
|
expect(SemanticsUpdateBuilderSpy.observations[1]!.childrenInTraversalOrder.length, 0);
|
|
expect(SemanticsUpdateBuilderSpy.observations[1]!.label, 'label');
|
|
expect(SemanticsUpdateBuilderSpy.observations[1]!.labelAttributes!.length, 1);
|
|
expect(
|
|
SemanticsUpdateBuilderSpy.observations[1]!.labelAttributes![0] is SpellOutStringAttribute,
|
|
isTrue,
|
|
);
|
|
expect(
|
|
SemanticsUpdateBuilderSpy.observations[1]!.labelAttributes![0].range,
|
|
const TextRange(start: 0, end: 5),
|
|
);
|
|
|
|
expect(SemanticsUpdateBuilderSpy.observations[1]!.value, 'value');
|
|
expect(SemanticsUpdateBuilderSpy.observations[1]!.valueAttributes!.length, 1);
|
|
expect(
|
|
SemanticsUpdateBuilderSpy.observations[1]!.valueAttributes![0] is LocaleStringAttribute,
|
|
isTrue,
|
|
);
|
|
final LocaleStringAttribute localeAttribute =
|
|
SemanticsUpdateBuilderSpy.observations[1]!.valueAttributes![0] as LocaleStringAttribute;
|
|
expect(localeAttribute.range, const TextRange(start: 0, end: 5));
|
|
expect(localeAttribute.locale, const Locale('en', 'MX'));
|
|
|
|
expect(SemanticsUpdateBuilderSpy.observations[1]!.hint, 'hint');
|
|
expect(SemanticsUpdateBuilderSpy.observations[1]!.hintAttributes!.length, 1);
|
|
expect(
|
|
SemanticsUpdateBuilderSpy.observations[1]!.hintAttributes![0] is SpellOutStringAttribute,
|
|
isTrue,
|
|
);
|
|
expect(
|
|
SemanticsUpdateBuilderSpy.observations[1]!.hintAttributes![0].range,
|
|
const TextRange(start: 1, end: 2),
|
|
);
|
|
|
|
expect(
|
|
tester.widget(find.byType(Semantics)).toString(),
|
|
'Semantics('
|
|
'container: false, '
|
|
'properties: SemanticsProperties, '
|
|
'attributedLabel: "label" [SpellOutStringAttribute(TextRange(start: 0, end: 5))], '
|
|
'attributedValue: "value" [LocaleStringAttribute(TextRange(start: 0, end: 5), en-MX)], '
|
|
'attributedHint: "hint" [SpellOutStringAttribute(TextRange(start: 1, end: 2))]' // ignore: missing_whitespace_between_adjacent_strings
|
|
')',
|
|
);
|
|
|
|
SemanticsUpdateBuilderSpy.observations.clear();
|
|
handle.dispose();
|
|
}, skip: true); // https://github.com/flutter/flutter/issues/97894
|
|
}
|
|
|
|
class SemanticsUpdateTestBinding extends AutomatedTestWidgetsFlutterBinding {
|
|
@override
|
|
ui.SemanticsUpdateBuilder createSemanticsUpdateBuilder() {
|
|
return SemanticsUpdateBuilderSpy();
|
|
}
|
|
}
|
|
|
|
class SemanticsUpdateBuilderSpy extends Fake implements ui.SemanticsUpdateBuilder {
|
|
final SemanticsUpdateBuilder _builder = ui.SemanticsUpdateBuilder();
|
|
|
|
static Map<int, SemanticsNodeUpdateObservation> observations =
|
|
<int, SemanticsNodeUpdateObservation>{};
|
|
|
|
@override
|
|
void updateNode({
|
|
required int id,
|
|
required int flags,
|
|
required int actions,
|
|
required int maxValueLength,
|
|
required int currentValueLength,
|
|
required int textSelectionBase,
|
|
required int textSelectionExtent,
|
|
required int platformViewId,
|
|
required int scrollChildren,
|
|
required int scrollIndex,
|
|
required double scrollPosition,
|
|
required double scrollExtentMax,
|
|
required double scrollExtentMin,
|
|
required double elevation,
|
|
required double thickness,
|
|
required Rect rect,
|
|
required String identifier,
|
|
required String label,
|
|
List<StringAttribute>? labelAttributes,
|
|
required String value,
|
|
List<StringAttribute>? valueAttributes,
|
|
required String increasedValue,
|
|
List<StringAttribute>? increasedValueAttributes,
|
|
required String decreasedValue,
|
|
List<StringAttribute>? decreasedValueAttributes,
|
|
required String hint,
|
|
List<StringAttribute>? hintAttributes,
|
|
String? tooltip,
|
|
TextDirection? textDirection,
|
|
required Float64List transform,
|
|
required Int32List childrenInTraversalOrder,
|
|
required Int32List childrenInHitTestOrder,
|
|
required Int32List additionalActions,
|
|
int headingLevel = 0,
|
|
String? linkUrl,
|
|
ui.SemanticsRole role = ui.SemanticsRole.none,
|
|
required List<String>? controlsNodes,
|
|
}) {
|
|
// Makes sure we don't send the same id twice.
|
|
assert(!observations.containsKey(id));
|
|
observations[id] = SemanticsNodeUpdateObservation(
|
|
label: label,
|
|
labelAttributes: labelAttributes,
|
|
hint: hint,
|
|
hintAttributes: hintAttributes,
|
|
value: value,
|
|
valueAttributes: valueAttributes,
|
|
childrenInTraversalOrder: childrenInTraversalOrder,
|
|
);
|
|
}
|
|
|
|
@override
|
|
void updateCustomAction({required int id, String? label, String? hint, int overrideId = -1}) =>
|
|
_builder.updateCustomAction(id: id, label: label, hint: hint, overrideId: overrideId);
|
|
|
|
@override
|
|
ui.SemanticsUpdate build() => _builder.build();
|
|
}
|
|
|
|
class SemanticsNodeUpdateObservation {
|
|
const SemanticsNodeUpdateObservation({
|
|
required this.label,
|
|
this.labelAttributes,
|
|
required this.value,
|
|
this.valueAttributes,
|
|
required this.hint,
|
|
this.hintAttributes,
|
|
required this.childrenInTraversalOrder,
|
|
});
|
|
|
|
final String label;
|
|
final List<StringAttribute>? labelAttributes;
|
|
final String value;
|
|
final List<StringAttribute>? valueAttributes;
|
|
final String hint;
|
|
final List<StringAttribute>? hintAttributes;
|
|
final Int32List childrenInTraversalOrder;
|
|
}
|