Several pairs of near-identical code paths differ in one load-bearing line. Each site now carries a comment naming the invariant that forces it apart, backed by a characterization test so a future deduplication fails loudly instead of silently changing behaviour. Pinned: focusable wrapper vs. chip D-pad activation policy, profile connection cleanup's raw-id vs. ServerId-typed server projections, live TV tab loaders, video player display matching and playback service wiring, track selection container ordering, tracker HTTP client status ladder, and the MediaServerHttpClient shutdown/cancellation contract versus ManagedHttpClient's closing guard. New tests: test/focus/dpad_activation_policy_test.dart test/services/track_selection_container_ordinal_test.dart test/services/trackers/tracker_status_ladder_test.dart test/utils/media_server_http_client_shutdown_test.dart
89 lines
3.1 KiB
Dart
89 lines
3.1 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:plezy/focus/focusable_wrapper.dart';
|
|
import 'package:plezy/widgets/focusable_tab_chip.dart';
|
|
|
|
void main() {
|
|
// FocusableWrapper and FocusableChipStateMixin run the same d-pad activation
|
|
// sequence under opposite consume policies. These pin the two differences that
|
|
// keep the handlers separate.
|
|
group('d-pad activation policies', () {
|
|
Future<List<LogicalKeyboardKey>> escapedKeysFor(
|
|
WidgetTester tester,
|
|
FocusNode node,
|
|
Widget child,
|
|
LogicalKeyboardKey key,
|
|
) async {
|
|
final escaped = <LogicalKeyboardKey>[];
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Scaffold(
|
|
body: Focus(
|
|
onKeyEvent: (_, event) {
|
|
if (event is KeyDownEvent) escaped.add(event.logicalKey);
|
|
return KeyEventResult.handled;
|
|
},
|
|
child: child,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
node.requestFocus();
|
|
await tester.pump();
|
|
|
|
await tester.sendKeyEvent(key);
|
|
await tester.pump();
|
|
return escaped;
|
|
}
|
|
|
|
testWidgets('wrapper consumes the context menu key with no onLongPress', (tester) async {
|
|
final node = FocusNode(debugLabel: 'card');
|
|
addTearDown(node.dispose);
|
|
|
|
final escaped = await escapedKeysFor(
|
|
tester,
|
|
node,
|
|
FocusableWrapper(focusNode: node, onSelect: () {}, child: const SizedBox(width: 10, height: 10)),
|
|
LogicalKeyboardKey.contextMenu,
|
|
);
|
|
|
|
expect(escaped, isEmpty);
|
|
});
|
|
|
|
testWidgets('chip leaves the context menu key to its ancestors with no onLongPress', (tester) async {
|
|
final node = FocusNode(debugLabel: 'chip');
|
|
addTearDown(node.dispose);
|
|
|
|
final escaped = await escapedKeysFor(
|
|
tester,
|
|
node,
|
|
FocusableTabChip(label: 'Tab', isSelected: true, focusNode: node, onSelect: () {}),
|
|
LogicalKeyboardKey.contextMenu,
|
|
);
|
|
|
|
expect(escaped, [LogicalKeyboardKey.contextMenu]);
|
|
});
|
|
|
|
testWidgets('wrapper passes unmapped RIGHT/DOWN through to the framework', (tester) async {
|
|
final node = FocusNode(debugLabel: 'card');
|
|
addTearDown(node.dispose);
|
|
Widget card() => FocusableWrapper(focusNode: node, onSelect: () {}, child: const SizedBox(width: 10, height: 10));
|
|
|
|
expect(await escapedKeysFor(tester, node, card(), LogicalKeyboardKey.arrowRight), [
|
|
LogicalKeyboardKey.arrowRight,
|
|
]);
|
|
expect(await escapedKeysFor(tester, node, card(), LogicalKeyboardKey.arrowDown), [LogicalKeyboardKey.arrowDown]);
|
|
});
|
|
|
|
testWidgets('chip traps unmapped RIGHT/DOWN so focus cannot escape the strip', (tester) async {
|
|
final node = FocusNode(debugLabel: 'chip');
|
|
addTearDown(node.dispose);
|
|
Widget chip() => FocusableTabChip(label: 'Tab', isSelected: true, focusNode: node, onSelect: () {});
|
|
|
|
expect(await escapedKeysFor(tester, node, chip(), LogicalKeyboardKey.arrowRight), isEmpty);
|
|
expect(await escapedKeysFor(tester, node, chip(), LogicalKeyboardKey.arrowDown), isEmpty);
|
|
});
|
|
});
|
|
}
|