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
145 lines
4.2 KiB
Dart
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));
|
|
});
|
|
}
|