216 lines
6.9 KiB
Dart
216 lines
6.9 KiB
Dart
import 'dart:ui' show SemanticsAction, Tristate;
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/semantics.dart' show SemanticsNode;
|
|
import 'package:flutter/services.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:plezy/focus/card_focus_scope.dart';
|
|
import 'package:plezy/focus/focusable_wrapper.dart';
|
|
import 'package:plezy/focus/input_mode_tracker.dart';
|
|
|
|
void main() {
|
|
Finder chromeIn(Type type) => find.descendant(of: find.byType(FocusableWrapper), matching: find.byType(type));
|
|
|
|
Widget buildWrapper() => Scaffold(
|
|
body: FocusableWrapper(onSelect: () {}, child: const SizedBox(width: 10, height: 10)),
|
|
);
|
|
|
|
testWidgets('pointer mode builds no focus chrome around the child', (tester) async {
|
|
await tester.pumpWidget(MaterialApp(home: buildWrapper()));
|
|
|
|
expect(chromeIn(Transform), findsNothing);
|
|
expect(chromeIn(AnimatedContainer), findsNothing);
|
|
expect(chromeIn(Focus), findsWidgets);
|
|
});
|
|
|
|
testWidgets('keyboard mode builds the scale/border chrome', (tester) async {
|
|
await tester.pumpWidget(InputModeTracker(child: MaterialApp(home: buildWrapper())));
|
|
|
|
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
|
|
await tester.pump();
|
|
|
|
expect(chromeIn(AnimatedBuilder), findsOneWidget);
|
|
expect(chromeIn(AnimatedContainer), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('focusing in pointer mode works without a pre-built controller', (tester) async {
|
|
final node = FocusNode(debugLabel: 'card');
|
|
addTearDown(node.dispose);
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Scaffold(
|
|
body: FocusableWrapper(focusNode: node, onSelect: () {}, child: const SizedBox(width: 10, height: 10)),
|
|
),
|
|
),
|
|
);
|
|
|
|
// The AnimationController is created lazily on first focus; gaining and
|
|
// losing focus in pointer mode must not throw.
|
|
node.requestFocus();
|
|
await tester.pump();
|
|
node.unfocus();
|
|
await tester.pump();
|
|
|
|
expect(tester.takeException(), isNull);
|
|
});
|
|
|
|
testWidgets('context menu key does not suppress the next select', (tester) async {
|
|
final node = FocusNode(debugLabel: 'card');
|
|
addTearDown(node.dispose);
|
|
var selected = 0;
|
|
var longPressed = 0;
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Scaffold(
|
|
body: FocusableWrapper(
|
|
focusNode: node,
|
|
onSelect: () => selected++,
|
|
onLongPress: () => longPressed++,
|
|
child: const SizedBox(width: 10, height: 10),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
node.requestFocus();
|
|
await tester.pump();
|
|
|
|
await tester.sendKeyEvent(LogicalKeyboardKey.contextMenu);
|
|
await tester.sendKeyEvent(LogicalKeyboardKey.enter);
|
|
|
|
expect(longPressed, 1);
|
|
expect(selected, 1);
|
|
});
|
|
|
|
testWidgets('focus scale animation keeps child semantics geometry stable', (tester) async {
|
|
final semantics = tester.ensureSemantics();
|
|
final node = FocusNode(debugLabel: 'card');
|
|
addTearDown(node.dispose);
|
|
|
|
await tester.pumpWidget(
|
|
InputModeTracker(
|
|
child: MaterialApp(
|
|
home: Scaffold(
|
|
body: Center(
|
|
child: FocusableWrapper(
|
|
focusNode: node,
|
|
focusScale: 1.2,
|
|
delegateFocusBorder: true,
|
|
child: CardFocusBorder(
|
|
child: Semantics(label: 'card content', child: SizedBox(width: 100, height: 100)),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
|
|
node.unfocus();
|
|
await tester.pumpAndSettle();
|
|
node.requestFocus();
|
|
await tester.pump();
|
|
final semanticsOwner = tester.binding.rootPipelineOwner.semanticsOwner!;
|
|
var semanticsUpdates = 0;
|
|
void countSemanticsUpdate() => semanticsUpdates++;
|
|
semanticsOwner.addListener(countSemanticsUpdate);
|
|
|
|
await tester.pump(const Duration(milliseconds: 16));
|
|
semanticsUpdates = 0;
|
|
await tester.pump(const Duration(milliseconds: 16));
|
|
|
|
expect(semanticsUpdates, 0);
|
|
semanticsOwner.removeListener(countSemanticsUpdate);
|
|
semantics.dispose();
|
|
});
|
|
|
|
testWidgets('semantic label replaces child semantics by default', (tester) async {
|
|
final semantics = tester.ensureSemantics();
|
|
var activations = 0;
|
|
var childActivations = 0;
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Scaffold(
|
|
body: FocusableWrapper(
|
|
semanticLabel: 'Open details',
|
|
semanticValue: 'Ready',
|
|
onSelect: () => activations++,
|
|
child: Semantics(
|
|
label: 'Decorative artwork',
|
|
value: 'Decorative state',
|
|
button: true,
|
|
onTap: () => childActivations++,
|
|
child: const Text('Poster'),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
final finder = find.bySemanticsLabel('Open details');
|
|
expect(finder, findsOneWidget);
|
|
expect(find.bySemanticsLabel('Decorative artwork'), findsNothing);
|
|
expect(find.bySemanticsLabel('Poster'), findsNothing);
|
|
|
|
final node = tester.getSemantics(finder);
|
|
final data = node.getSemanticsData();
|
|
expect(data.value, 'Ready');
|
|
expect(data.flagsCollection.isButton, isTrue);
|
|
expect(data.flagsCollection.isEnabled, Tristate.isTrue);
|
|
expect(data.hasAction(SemanticsAction.tap), isTrue);
|
|
expect(_semanticTapNodeCount(tester), 1);
|
|
|
|
node.owner!.performAction(node.id, SemanticsAction.tap);
|
|
expect(activations, 1);
|
|
expect(childActivations, 0);
|
|
semantics.dispose();
|
|
});
|
|
|
|
testWidgets('merge mode supplements non-interactive child semantics', (tester) async {
|
|
final semantics = tester.ensureSemantics();
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Scaffold(
|
|
body: FocusableWrapper(
|
|
semanticLabel: 'Expand',
|
|
excludeChildSemantics: false,
|
|
onSelect: () {},
|
|
child: Semantics(value: 'Available offline', child: const Text('Visible synopsis')),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
final finder = find.bySemanticsLabel(RegExp('Expand'));
|
|
expect(finder, findsOneWidget);
|
|
expect(find.bySemanticsLabel(RegExp('Visible synopsis')), findsOneWidget);
|
|
|
|
final data = tester.getSemantics(finder).getSemanticsData();
|
|
expect(data.label, contains('Expand'));
|
|
expect(data.label, contains('Visible synopsis'));
|
|
expect(data.value, 'Available offline');
|
|
expect(data.flagsCollection.isButton, isTrue);
|
|
expect(data.flagsCollection.isEnabled, Tristate.isTrue);
|
|
expect(data.hasAction(SemanticsAction.tap), isTrue);
|
|
expect(_semanticTapNodeCount(tester), 1);
|
|
semantics.dispose();
|
|
});
|
|
}
|
|
|
|
int _semanticTapNodeCount(WidgetTester tester) {
|
|
var count = 0;
|
|
void visit(SemanticsNode node) {
|
|
if (node.getSemanticsData().hasAction(SemanticsAction.tap)) count++;
|
|
node.visitChildren((child) {
|
|
visit(child);
|
|
return true;
|
|
});
|
|
}
|
|
|
|
visit(tester.binding.renderViews.single.owner!.semanticsOwner!.rootSemanticsNode!);
|
|
return count;
|
|
}
|