
Summary: - Add `key` field to `SemanticsNode`, while moving key into `foundation` library so it can be used by the render layer. - Introduce `SemanticsProperties` and move many of the `Semantics` fields into it. - Introduce `CustomPaintSemantics` - a `SemanticsNode` prototype created by `CustomPainter`. - Introduce `semanticsBuilder` and `shouldRebuildSemantics` in `CustomerPainter` **Breaking change** The default `Semantics` constructor becomes non-const (due to https://github.com/dart-lang/sdk/issues/20962). However, a new `const Semantics.fromProperties` is added that still allowed creating constant `Semantics` widgets ([mailing list announcement](https://groups.google.com/forum/#!topic/flutter-dev/KQXBl2_1sws)). Fixes https://github.com/flutter/flutter/issues/11791 Fixes https://github.com/flutter/flutter/issues/1666
550 lines
16 KiB
Dart
550 lines
16 KiB
Dart
// Copyright 2015 The Chromium 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/material.dart';
|
|
import 'package:flutter/rendering.dart';
|
|
import 'package:flutter/semantics.dart';
|
|
import 'package:flutter/widgets.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
|
|
import 'semantics_tester.dart';
|
|
|
|
void main() {
|
|
testWidgets('Semantics shutdown and restart', (WidgetTester tester) async {
|
|
SemanticsTester semantics = new SemanticsTester(tester);
|
|
|
|
final TestSemantics expectedSemantics = new TestSemantics.root(
|
|
children: <TestSemantics>[
|
|
new TestSemantics.rootChild(
|
|
label: 'test1',
|
|
textDirection: TextDirection.ltr,
|
|
)
|
|
],
|
|
);
|
|
|
|
await tester.pumpWidget(
|
|
new Container(
|
|
child: new Semantics(
|
|
label: 'test1',
|
|
textDirection: TextDirection.ltr,
|
|
child: new Container()
|
|
)
|
|
)
|
|
);
|
|
|
|
expect(semantics, hasSemantics(
|
|
expectedSemantics,
|
|
ignoreTransform: true,
|
|
ignoreRect: true,
|
|
ignoreId: true,
|
|
));
|
|
|
|
semantics.dispose();
|
|
semantics = null;
|
|
|
|
expect(tester.binding.hasScheduledFrame, isFalse);
|
|
semantics = new SemanticsTester(tester);
|
|
expect(tester.binding.hasScheduledFrame, isTrue);
|
|
await tester.pump();
|
|
|
|
expect(semantics, hasSemantics(
|
|
expectedSemantics,
|
|
ignoreTransform: true,
|
|
ignoreRect: true,
|
|
ignoreId: true,
|
|
));
|
|
semantics.dispose();
|
|
});
|
|
|
|
testWidgets('Detach and reattach assert', (WidgetTester tester) async {
|
|
final SemanticsTester semantics = new SemanticsTester(tester);
|
|
final GlobalKey key = new GlobalKey();
|
|
|
|
await tester.pumpWidget(new Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: new Container(
|
|
child: new Semantics(
|
|
label: 'test1',
|
|
child: new Semantics(
|
|
key: key,
|
|
container: true,
|
|
label: 'test2a',
|
|
child: new Container()
|
|
)
|
|
)
|
|
)
|
|
));
|
|
|
|
expect(semantics, hasSemantics(
|
|
new TestSemantics.root(
|
|
children: <TestSemantics>[
|
|
new TestSemantics.rootChild(
|
|
label: 'test1',
|
|
children: <TestSemantics>[
|
|
new TestSemantics(
|
|
label: 'test2a',
|
|
)
|
|
]
|
|
)
|
|
]
|
|
),
|
|
ignoreId: true,
|
|
ignoreRect: true,
|
|
ignoreTransform: true,
|
|
));
|
|
|
|
await tester.pumpWidget(new Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: new Container(
|
|
child: new Semantics(
|
|
label: 'test1',
|
|
child: new Semantics(
|
|
container: true,
|
|
label: 'middle',
|
|
child: new Semantics(
|
|
key: key,
|
|
container: true,
|
|
label: 'test2b',
|
|
child: new Container()
|
|
)
|
|
)
|
|
)
|
|
)
|
|
));
|
|
|
|
expect(semantics, hasSemantics(
|
|
new TestSemantics.root(
|
|
children: <TestSemantics>[
|
|
new TestSemantics.rootChild(
|
|
label: 'test1',
|
|
children: <TestSemantics>[
|
|
new TestSemantics(
|
|
label: 'middle',
|
|
children: <TestSemantics>[
|
|
new TestSemantics(
|
|
label: 'test2b',
|
|
),
|
|
],
|
|
)
|
|
]
|
|
)
|
|
]
|
|
),
|
|
ignoreId: true,
|
|
ignoreRect: true,
|
|
ignoreTransform: true,
|
|
));
|
|
|
|
semantics.dispose();
|
|
});
|
|
|
|
testWidgets('Semantics and Directionality - RTL', (WidgetTester tester) async {
|
|
final SemanticsTester semantics = new SemanticsTester(tester);
|
|
|
|
await tester.pumpWidget(
|
|
new Directionality(
|
|
textDirection: TextDirection.rtl,
|
|
child: new Semantics(
|
|
label: 'test1',
|
|
child: new Container(),
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(semantics, includesNodeWith(label: 'test1', textDirection: TextDirection.rtl));
|
|
});
|
|
|
|
testWidgets('Semantics and Directionality - LTR', (WidgetTester tester) async {
|
|
final SemanticsTester semantics = new SemanticsTester(tester);
|
|
|
|
await tester.pumpWidget(
|
|
new Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: new Semantics(
|
|
label: 'test1',
|
|
child: new Container(),
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(semantics, includesNodeWith(label: 'test1', textDirection: TextDirection.ltr));
|
|
});
|
|
|
|
testWidgets('Semantics and Directionality - cannot override RTL with LTR', (WidgetTester tester) async {
|
|
final SemanticsTester semantics = new SemanticsTester(tester);
|
|
|
|
final TestSemantics expectedSemantics = new TestSemantics.root(
|
|
children: <TestSemantics>[
|
|
new TestSemantics.rootChild(
|
|
label: 'test1',
|
|
textDirection: TextDirection.ltr,
|
|
)
|
|
]
|
|
);
|
|
|
|
await tester.pumpWidget(
|
|
new Directionality(
|
|
textDirection: TextDirection.rtl,
|
|
child: new Semantics(
|
|
label: 'test1',
|
|
textDirection: TextDirection.ltr,
|
|
child: new Container(),
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(semantics, hasSemantics(expectedSemantics, ignoreTransform: true, ignoreRect: true, ignoreId: true));
|
|
});
|
|
|
|
testWidgets('Semantics and Directionality - cannot override LTR with RTL', (WidgetTester tester) async {
|
|
final SemanticsTester semantics = new SemanticsTester(tester);
|
|
|
|
final TestSemantics expectedSemantics = new TestSemantics.root(
|
|
children: <TestSemantics>[
|
|
new TestSemantics.rootChild(
|
|
label: 'test1',
|
|
textDirection: TextDirection.rtl,
|
|
)
|
|
]
|
|
);
|
|
|
|
await tester.pumpWidget(
|
|
new Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: new Semantics(
|
|
label: 'test1',
|
|
textDirection: TextDirection.rtl,
|
|
child: new Container(),
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(semantics, hasSemantics(expectedSemantics, ignoreTransform: true, ignoreRect: true, ignoreId: true));
|
|
});
|
|
|
|
testWidgets('Semantics label and hint', (WidgetTester tester) async {
|
|
final SemanticsTester semantics = new SemanticsTester(tester);
|
|
|
|
await tester.pumpWidget(
|
|
new Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: new Semantics(
|
|
label: 'label',
|
|
hint: 'hint',
|
|
value: 'value',
|
|
child: new Container(),
|
|
),
|
|
),
|
|
);
|
|
|
|
final TestSemantics expectedSemantics = new TestSemantics.root(
|
|
children: <TestSemantics>[
|
|
new TestSemantics.rootChild(
|
|
label: 'label',
|
|
hint: 'hint',
|
|
value: 'value',
|
|
textDirection: TextDirection.ltr,
|
|
)
|
|
]
|
|
);
|
|
|
|
expect(semantics, hasSemantics(expectedSemantics, ignoreTransform: true, ignoreRect: true, ignoreId: true));
|
|
});
|
|
|
|
testWidgets('Semantics hints can merge', (WidgetTester tester) async {
|
|
final SemanticsTester semantics = new SemanticsTester(tester);
|
|
|
|
await tester.pumpWidget(
|
|
new Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: new Semantics(
|
|
container: true,
|
|
child: new Column(
|
|
children: <Widget>[
|
|
new Semantics(
|
|
hint: 'hint one',
|
|
),
|
|
new Semantics(
|
|
hint: 'hint two',
|
|
)
|
|
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
final TestSemantics expectedSemantics = new TestSemantics.root(
|
|
children: <TestSemantics>[
|
|
new TestSemantics.rootChild(
|
|
hint: 'hint one\nhint two',
|
|
textDirection: TextDirection.ltr,
|
|
)
|
|
]
|
|
);
|
|
|
|
expect(semantics, hasSemantics(expectedSemantics, ignoreTransform: true, ignoreRect: true, ignoreId: true));
|
|
});
|
|
|
|
testWidgets('Semantics values do not merge', (WidgetTester tester) async {
|
|
final SemanticsTester semantics = new SemanticsTester(tester);
|
|
|
|
await tester.pumpWidget(
|
|
new Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: new Semantics(
|
|
container: true,
|
|
child: new Column(
|
|
children: <Widget>[
|
|
new Semantics(
|
|
value: 'value one',
|
|
child: new Container(
|
|
height: 10.0,
|
|
width: 10.0,
|
|
)
|
|
),
|
|
new Semantics(
|
|
value: 'value two',
|
|
child: new Container(
|
|
height: 10.0,
|
|
width: 10.0,
|
|
)
|
|
)
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
final TestSemantics expectedSemantics = new TestSemantics.root(
|
|
children: <TestSemantics>[
|
|
new TestSemantics.rootChild(
|
|
children: <TestSemantics>[
|
|
new TestSemantics(
|
|
value: 'value one',
|
|
textDirection: TextDirection.ltr,
|
|
),
|
|
new TestSemantics(
|
|
value: 'value two',
|
|
textDirection: TextDirection.ltr,
|
|
),
|
|
]
|
|
)
|
|
],
|
|
);
|
|
|
|
expect(semantics, hasSemantics(expectedSemantics, ignoreTransform: true, ignoreRect: true, ignoreId: true));
|
|
});
|
|
|
|
testWidgets('Semantics value and hint can merge', (WidgetTester tester) async {
|
|
final SemanticsTester semantics = new SemanticsTester(tester);
|
|
|
|
await tester.pumpWidget(
|
|
new Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: new Semantics(
|
|
container: true,
|
|
child: new Column(
|
|
children: <Widget>[
|
|
new Semantics(
|
|
hint: 'hint',
|
|
),
|
|
new Semantics(
|
|
value: 'value',
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
final TestSemantics expectedSemantics = new TestSemantics.root(
|
|
children: <TestSemantics>[
|
|
new TestSemantics.rootChild(
|
|
hint: 'hint',
|
|
value: 'value',
|
|
textDirection: TextDirection.ltr,
|
|
)
|
|
]
|
|
);
|
|
|
|
expect(semantics, hasSemantics(expectedSemantics, ignoreTransform: true, ignoreRect: true, ignoreId: true));
|
|
});
|
|
|
|
testWidgets('Semantics widget supports all actions', (WidgetTester tester) async {
|
|
final SemanticsTester semantics = new SemanticsTester(tester);
|
|
|
|
final List<SemanticsAction> performedActions = <SemanticsAction>[];
|
|
|
|
await tester.pumpWidget(
|
|
new Semantics(
|
|
container: true,
|
|
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),
|
|
)
|
|
);
|
|
|
|
final Set<SemanticsAction> allActions = SemanticsAction.values.values.toSet()
|
|
..remove(SemanticsAction.showOnScreen); // showOnScreen is non user-exposed.
|
|
|
|
final int expectedId = 32;
|
|
final TestSemantics expectedSemantics = new TestSemantics.root(
|
|
children: <TestSemantics>[
|
|
new TestSemantics.rootChild(
|
|
id: expectedId,
|
|
rect: TestSemantics.fullScreen,
|
|
actions: allActions.fold(0, (int previous, SemanticsAction action) => previous | action.index)
|
|
),
|
|
],
|
|
);
|
|
expect(semantics, hasSemantics(expectedSemantics));
|
|
|
|
// Do the actions work?
|
|
final SemanticsOwner semanticsOwner = tester.binding.pipelineOwner.semanticsOwner;
|
|
int expectedLength = 1;
|
|
for (SemanticsAction action in allActions) {
|
|
semanticsOwner.performAction(expectedId, action);
|
|
expect(performedActions.length, expectedLength);
|
|
expect(performedActions.last, action);
|
|
expectedLength += 1;
|
|
}
|
|
|
|
semantics.dispose();
|
|
});
|
|
|
|
testWidgets('Actions can be replaced without triggering semantics update', (WidgetTester tester) async {
|
|
final SemanticsTester semantics = new SemanticsTester(tester);
|
|
int semanticsUpdateCount = 0;
|
|
tester.binding.pipelineOwner.ensureSemantics(
|
|
listener: () {
|
|
semanticsUpdateCount += 1;
|
|
}
|
|
);
|
|
|
|
final List<String> performedActions = <String>[];
|
|
|
|
await tester.pumpWidget(
|
|
new Semantics(
|
|
container: true,
|
|
onTap: () => performedActions.add('first'),
|
|
),
|
|
);
|
|
|
|
final int expectedId = 35;
|
|
final TestSemantics expectedSemantics = new TestSemantics.root(
|
|
children: <TestSemantics>[
|
|
new 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(
|
|
new 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(
|
|
new Semantics(
|
|
container: true,
|
|
onTap: () => performedActions.add('second'),
|
|
onLongPress: () => performedActions.add('longPress'),
|
|
),
|
|
);
|
|
|
|
final TestSemantics expectedSemanticsWithLongPress = new TestSemantics.root(
|
|
children: <TestSemantics>[
|
|
new 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(
|
|
new Semantics(
|
|
container: true,
|
|
onTap: () => performedActions.add('second'),
|
|
),
|
|
);
|
|
|
|
expect(semantics, hasSemantics(expectedSemantics));
|
|
expect(semanticsUpdateCount, 1);
|
|
|
|
semantics.dispose();
|
|
});
|
|
|
|
testWidgets('Increased/decreased values are annotated', (WidgetTester tester) async {
|
|
final SemanticsTester semantics = new SemanticsTester(tester);
|
|
|
|
await tester.pumpWidget(
|
|
new Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: new Semantics(
|
|
container: true,
|
|
value: '10s',
|
|
increasedValue: '11s',
|
|
decreasedValue: '9s',
|
|
onIncrease: () => () {},
|
|
onDecrease: () => () {},
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(semantics, hasSemantics(new TestSemantics.root(
|
|
children: <TestSemantics>[
|
|
new 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();
|
|
});
|
|
}
|