Files
plezy/test/widgets/focusable_list_tile_test.dart
T
edde746 4522f42bbf fix(focus): reveal focused items only during keyboard/D-pad sessions
Closing an episode context menu (or its Rate / File Info sheets) on a
show opened from a Home section scrolled the detail page back to the
top, and on shows with many seasons jumped the season selector back to
the entered-from season. The Home path parks invisible focus on the
initial episode/season target; dismissing the menu restored focus to
that parked node, whose focus-gain auto-scroll then yanked the viewport.

Focus chrome is already keyboard-mode-only, so make the focus-gain
reveal match: FocusableWrapper, FocusableChipStateMixin, and
FocusableTileStateMixin now scroll into view only during keyboard/D-pad
sessions. The gate reads the tracker's live state (new
InputModeTracker.currentMode) because the inherited provider is one
frame stale on the first navigation key of a session. The touch OSK
search submit keeps its jump-to-results via an explicit reveal, matching
the existing pointer-mode convention of pairing requestFocus with an
explicit scroll.

close #2031
2026-08-19 22:38:18 +02:00

145 lines
4.2 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:plezy/focus/input_mode_tracker.dart';
import 'package:plezy/widgets/focusable_list_tile.dart';
void main() {
testWidgets('switch tile toggles once from SELECT', (tester) async {
final focusNode = FocusNode(debugLabel: 'switch');
addTearDown(focusNode.dispose);
var value = false;
var changes = 0;
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: StatefulBuilder(
builder: (context, setState) => FocusableSwitchListTile(
focusNode: focusNode,
value: value,
title: const Text('Switch'),
onChanged: (next) {
changes++;
setState(() => value = next);
},
),
),
),
),
);
focusNode.requestFocus();
await tester.pump();
await tester.sendKeyEvent(LogicalKeyboardKey.enter);
await tester.pump();
expect(value, isTrue);
expect(changes, 1);
});
testWidgets('checkbox tile toggles once from SELECT', (tester) async {
final focusNode = FocusNode(debugLabel: 'checkbox');
addTearDown(focusNode.dispose);
var value = false;
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: StatefulBuilder(
builder: (context, setState) => FocusableCheckboxListTile(
focusNode: focusNode,
value: value,
title: const Text('Checkbox'),
onChanged: (next) => setState(() => value = next ?? false),
),
),
),
),
);
focusNode.requestFocus();
await tester.pump();
await tester.sendKeyEvent(LogicalKeyboardKey.enter);
await tester.pump();
expect(value, isTrue);
});
testWidgets('disabled switch tile cannot be focused or activated', (tester) async {
final focusNode = FocusNode(debugLabel: 'disabled switch');
addTearDown(focusNode.dispose);
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: FocusableSwitchListTile(
focusNode: focusNode,
value: false,
title: const Text('Disabled'),
onChanged: null,
),
),
),
);
focusNode.requestFocus();
await tester.pump();
expect(focusNode.hasFocus, isFalse);
});
Widget buildScrollableTiles({required ScrollController controller, required FocusNode bottomNode}) {
return InputModeTracker(
child: MaterialApp(
home: Scaffold(
body: SingleChildScrollView(
controller: controller,
child: Column(
children: [
for (var i = 0; i < 30; i++) const SizedBox(height: 56, width: 100),
FocusableListTile(focusNode: bottomNode, title: const Text('Target'), onTap: () {}),
],
),
),
),
),
);
}
testWidgets('pointer-mode focus gain does not reveal the tile', (tester) async {
final bottomNode = FocusNode(debugLabel: 'tile');
final controller = ScrollController();
addTearDown(bottomNode.dispose);
addTearDown(controller.dispose);
await tester.pumpWidget(buildScrollableTiles(controller: controller, bottomNode: bottomNode));
// Programmatic focus without a keyboard session (e.g. a context menu
// restoring focus on close, issue #2031) must not move the viewport.
bottomNode.requestFocus();
await tester.pumpAndSettle();
expect(bottomNode.hasFocus, isTrue);
expect(controller.offset, 0);
});
testWidgets('keyboard-mode focus gain centers the tile', (tester) async {
final bottomNode = FocusNode(debugLabel: 'tile');
final controller = ScrollController();
addTearDown(bottomNode.dispose);
addTearDown(controller.dispose);
await tester.pumpWidget(buildScrollableTiles(controller: controller, bottomNode: bottomNode));
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
await tester.pump();
bottomNode.requestFocus();
await tester.pumpAndSettle();
expect(bottomNode.hasFocus, isTrue);
expect(controller.offset, greaterThan(0));
});
}