flutter/packages/flutter/test/widgets/semantics_test.dart
Yegor ffb24eda56
Accessibility API for CustomPainter (#13313)
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
2017-12-04 19:49:14 -08:00

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();
});
}