feat: add D-pad navigation for phones
This commit is contained in:
parent
414f8e8484
commit
b3df7fba9f
6 changed files with 380 additions and 31 deletions
|
|
@ -172,6 +172,115 @@ void main() {
|
|||
expect(find.byType(NoteEditorScreen), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('holding the D-pad center goes back to the list',
|
||||
(tester) async {
|
||||
await tester.runAsync(() async {
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
home: Builder(
|
||||
builder: (context) => TextButton(
|
||||
onPressed: () => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => NoteEditorScreen(
|
||||
note: note, notesDirectoryPath: note.parent.path),
|
||||
),
|
||||
),
|
||||
child: const Text('list'),
|
||||
),
|
||||
),
|
||||
));
|
||||
await tester.tap(find.text('list'));
|
||||
await tester.pump();
|
||||
await Future<void>.delayed(const Duration(milliseconds: 100));
|
||||
});
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.sendKeyDownEvent(LogicalKeyboardKey.select);
|
||||
await tester.pump(const Duration(milliseconds: 200));
|
||||
await tester.sendKeyUpEvent(LogicalKeyboardKey.select);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byType(NoteEditorScreen), findsOneWidget,
|
||||
reason: 'a short press stays in the note');
|
||||
|
||||
await tester.sendKeyDownEvent(LogicalKeyboardKey.select);
|
||||
await tester.pump(const Duration(milliseconds: 600));
|
||||
await tester.sendKeyUpEvent(LogicalKeyboardKey.select);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byType(NoteEditorScreen), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('D-pad Right opens the menu and Left goes back to the list',
|
||||
(tester) async {
|
||||
await tester.runAsync(() async {
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
home: Builder(
|
||||
builder: (context) => TextButton(
|
||||
onPressed: () => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => NoteEditorScreen(
|
||||
note: note, notesDirectoryPath: note.parent.path),
|
||||
),
|
||||
),
|
||||
child: const Text('list'),
|
||||
),
|
||||
),
|
||||
));
|
||||
await tester.tap(find.text('list'));
|
||||
await tester.pump();
|
||||
await Future<void>.delayed(const Duration(milliseconds: 100));
|
||||
});
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await _press(tester, LogicalKeyboardKey.arrowRight);
|
||||
expect(find.text('Save'), findsOneWidget);
|
||||
// Share has focus first; Down moves to Save, which closes the menu.
|
||||
await _press(tester, LogicalKeyboardKey.arrowDown);
|
||||
await _press(tester, LogicalKeyboardKey.select);
|
||||
expect(find.text('Save'), findsNothing);
|
||||
|
||||
await _press(tester, LogicalKeyboardKey.keyE, control: true);
|
||||
await _press(tester, LogicalKeyboardKey.arrowRight);
|
||||
expect(find.text('Save'), findsNothing,
|
||||
reason: 'arrows move the caret in edit mode');
|
||||
await _press(tester, LogicalKeyboardKey.arrowLeft);
|
||||
expect(find.byType(NoteEditorScreen), findsOneWidget);
|
||||
|
||||
await _press(tester, LogicalKeyboardKey.keyE, control: true);
|
||||
await _press(tester, LogicalKeyboardKey.arrowLeft);
|
||||
expect(find.byType(NoteEditorScreen), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('the D-pad moves past the font size slider in the menu',
|
||||
(tester) async {
|
||||
await pumpEditor(tester);
|
||||
await _press(tester, LogicalKeyboardKey.arrowRight);
|
||||
|
||||
String focusedLabel() {
|
||||
final context = FocusManager.instance.primaryFocus!.context!;
|
||||
if (context.findAncestorWidgetOfExactType<Slider>() != null ||
|
||||
context.widget is Slider) {
|
||||
return 'slider';
|
||||
}
|
||||
final texts = find.descendant(
|
||||
of: find.byWidget(context.widget), matching: find.byType(Text));
|
||||
return tester.widgetList<Text>(texts).map((t) => t.data).join();
|
||||
}
|
||||
|
||||
// Share, Save, Autosave, slider, Preview.
|
||||
for (var i = 0; i < 3; i++) {
|
||||
await _press(tester, LogicalKeyboardKey.arrowDown);
|
||||
}
|
||||
expect(focusedLabel(), 'slider');
|
||||
|
||||
await _press(tester, LogicalKeyboardKey.arrowRight);
|
||||
expect(tester.widget<MarkdownView>(find.byType(MarkdownView)).fontSize, 18,
|
||||
reason: 'Right adjusts the slider');
|
||||
|
||||
await _press(tester, LogicalKeyboardKey.arrowDown);
|
||||
expect(focusedLabel(), 'Preview Mode');
|
||||
});
|
||||
|
||||
testWidgets('the open note follows theme switches', (tester) async {
|
||||
final mode = ValueNotifier(ThemeMode.light);
|
||||
await tester.runAsync(() async {
|
||||
|
|
|
|||
128
test/note_list_dpad_test.dart
Normal file
128
test/note_list_dpad_test.dart
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:muzzle_velocity/note_editor.dart';
|
||||
import 'package:muzzle_velocity/note_list.dart';
|
||||
import 'package:path_provider_platform_interface/path_provider_platform_interface.dart';
|
||||
import 'package:plugin_platform_interface/plugin_platform_interface.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
class _FakePathProvider extends PathProviderPlatform
|
||||
with MockPlatformInterfaceMixin {
|
||||
_FakePathProvider(this.path);
|
||||
final String path;
|
||||
|
||||
@override
|
||||
Future<String?> getApplicationDocumentsPath() async => path;
|
||||
}
|
||||
|
||||
Future<void> _press(WidgetTester tester, LogicalKeyboardKey key) async {
|
||||
await tester.sendKeyEvent(key);
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
void main() {
|
||||
late Directory notes;
|
||||
|
||||
setUp(() {
|
||||
notes = Directory.systemTemp.createTempSync('mv-list');
|
||||
for (final name in ['alpha', 'beta', 'gamma']) {
|
||||
File('${notes.path}/$name.md').writeAsStringSync('# $name\n');
|
||||
}
|
||||
PathProviderPlatform.instance = _FakePathProvider(notes.path);
|
||||
SharedPreferences.setMockInitialValues({'initialized': true});
|
||||
});
|
||||
|
||||
Future<void> pumpList(WidgetTester tester) async {
|
||||
await tester.runAsync(() async {
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
home: NoteListScreen(
|
||||
toggleTheme: (_) {},
|
||||
toggleTags: () {},
|
||||
isDarkMode: false,
|
||||
showTags: true,
|
||||
),
|
||||
));
|
||||
await Future<void>.delayed(const Duration(milliseconds: 200));
|
||||
});
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
bool searchHasFocus(WidgetTester tester) =>
|
||||
tester.widget<TextField>(find.byType(TextField)).focusNode!.hasFocus;
|
||||
|
||||
List<String> highlighted(WidgetTester tester) => [
|
||||
for (final tile in tester.widgetList<ListTile>(find.byType(ListTile)))
|
||||
if (tile.selected) (tile.title as Text).data!,
|
||||
];
|
||||
|
||||
testWidgets('Down enters the list, Up and Left return to the search field',
|
||||
(tester) async {
|
||||
await pumpList(tester);
|
||||
expect(searchHasFocus(tester), isTrue);
|
||||
|
||||
await _press(tester, LogicalKeyboardKey.arrowDown);
|
||||
expect(searchHasFocus(tester), isFalse);
|
||||
expect(highlighted(tester), hasLength(1));
|
||||
final first = highlighted(tester).single;
|
||||
|
||||
await _press(tester, LogicalKeyboardKey.arrowDown);
|
||||
expect(highlighted(tester).single, isNot(first));
|
||||
|
||||
await _press(tester, LogicalKeyboardKey.arrowUp);
|
||||
expect(highlighted(tester).single, first);
|
||||
|
||||
await _press(tester, LogicalKeyboardKey.arrowUp);
|
||||
expect(searchHasFocus(tester), isTrue);
|
||||
expect(highlighted(tester), isEmpty);
|
||||
|
||||
await _press(tester, LogicalKeyboardKey.arrowDown);
|
||||
await _press(tester, LogicalKeyboardKey.arrowLeft);
|
||||
expect(searchHasFocus(tester), isTrue);
|
||||
expect(highlighted(tester), isEmpty);
|
||||
});
|
||||
|
||||
testWidgets('Right clears the search text, then opens the menu',
|
||||
(tester) async {
|
||||
await pumpList(tester);
|
||||
await tester.enterText(find.byType(TextField), 'alp');
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await _press(tester, LogicalKeyboardKey.arrowRight);
|
||||
expect(find.text('alp'), findsNothing);
|
||||
|
||||
await _press(tester, LogicalKeyboardKey.arrowRight);
|
||||
expect(find.text('About'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('Right in the list opens the menu with D-pad navigation',
|
||||
(tester) async {
|
||||
await pumpList(tester);
|
||||
await _press(tester, LogicalKeyboardKey.arrowDown);
|
||||
await _press(tester, LogicalKeyboardKey.arrowRight);
|
||||
expect(find.text('About'), findsOneWidget);
|
||||
|
||||
// The first item has focus, so moving down and selecting picks the second.
|
||||
await _press(tester, LogicalKeyboardKey.arrowDown);
|
||||
await _press(tester, LogicalKeyboardKey.select);
|
||||
expect(find.text('About'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('center opens the highlighted note', (tester) async {
|
||||
await pumpList(tester);
|
||||
await _press(tester, LogicalKeyboardKey.arrowDown);
|
||||
// The note reads its file with real I/O, so let it finish between frames.
|
||||
await tester.runAsync(() async {
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.select);
|
||||
for (var i = 0; i < 10; i++) {
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
await Future<void>.delayed(const Duration(milliseconds: 20));
|
||||
}
|
||||
});
|
||||
expect(find.byType(NoteEditorScreen), findsOneWidget);
|
||||
final editor = tester.widget<TextField>(find.descendant(
|
||||
of: find.byType(NoteEditorScreen), matching: find.byType(TextField)));
|
||||
expect(editor.controller!.text, matches(r'^# (alpha|beta|gamma)\n$'));
|
||||
});
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue