Files
plezy/test/focus/focusable_wrapper_test.dart
T

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