2026-10-13 11:31:14 +03:00
|
|
|
import 'dart:io';
|
|
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
|
import 'package:flutter/services.dart';
|
|
|
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
|
|
|
import 'package:muzzle_velocity/markdown_editing.dart';
|
|
|
|
|
import 'package:muzzle_velocity/markdown_view.dart';
|
|
|
|
|
import 'package:muzzle_velocity/note_editor.dart';
|
|
|
|
|
import 'package:muzzle_velocity/themes/day.dart';
|
|
|
|
|
import 'package:muzzle_velocity/themes/night.dart';
|
|
|
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
|
|
|
|
|
|
|
|
Future<void> _press(WidgetTester tester, LogicalKeyboardKey key,
|
|
|
|
|
{bool control = false, bool shift = false}) async {
|
|
|
|
|
if (control) await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft);
|
|
|
|
|
if (shift) await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftLeft);
|
|
|
|
|
await tester.sendKeyEvent(key);
|
|
|
|
|
if (shift) await tester.sendKeyUpEvent(LogicalKeyboardKey.shiftLeft);
|
|
|
|
|
if (control) await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft);
|
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void main() {
|
|
|
|
|
late File note;
|
|
|
|
|
|
|
|
|
|
setUp(() {
|
|
|
|
|
SharedPreferences.setMockInitialValues({});
|
|
|
|
|
note = File('${Directory.systemTemp.createTempSync('mv').path}/n.md')
|
|
|
|
|
..writeAsStringSync('one word\ntwo word\n');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
Future<TextField> pumpEditor(WidgetTester tester,
|
|
|
|
|
{Future<void> Function()? onQuit}) async {
|
|
|
|
|
await tester.runAsync(() async {
|
|
|
|
|
await tester.pumpWidget(MaterialApp(
|
|
|
|
|
home: NoteEditorScreen(
|
|
|
|
|
note: note,
|
|
|
|
|
notesDirectoryPath: note.parent.path,
|
|
|
|
|
searchTerms: const ['word'],
|
|
|
|
|
onQuit: onQuit ?? () async {},
|
|
|
|
|
),
|
|
|
|
|
));
|
|
|
|
|
// File reads complete outside the fake clock.
|
|
|
|
|
await Future<void>.delayed(const Duration(milliseconds: 100));
|
|
|
|
|
});
|
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
|
return tester.widget<TextField>(find.byType(TextField));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TextField field(WidgetTester tester) =>
|
|
|
|
|
tester.widget<TextField>(find.byType(TextField));
|
|
|
|
|
|
|
|
|
|
testWidgets('Ctrl+E toggles editing and Esc leaves it', (tester) async {
|
|
|
|
|
expect((await pumpEditor(tester)).readOnly, isTrue);
|
|
|
|
|
|
|
|
|
|
await _press(tester, LogicalKeyboardKey.keyE, control: true);
|
|
|
|
|
expect(field(tester).readOnly, isFalse);
|
|
|
|
|
|
|
|
|
|
await _press(tester, LogicalKeyboardKey.escape);
|
|
|
|
|
expect(field(tester).readOnly, isTrue);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
testWidgets('Ctrl+B and Ctrl+I wrap the selection in edit mode',
|
|
|
|
|
(tester) async {
|
|
|
|
|
await pumpEditor(tester);
|
|
|
|
|
await _press(tester, LogicalKeyboardKey.keyE, control: true);
|
|
|
|
|
|
|
|
|
|
final controller = field(tester).controller!;
|
|
|
|
|
controller.selection = const TextSelection(baseOffset: 0, extentOffset: 3);
|
|
|
|
|
await _press(tester, LogicalKeyboardKey.keyB, control: true);
|
|
|
|
|
expect(controller.text, startsWith('**one** word'));
|
|
|
|
|
|
|
|
|
|
await _press(tester, LogicalKeyboardKey.keyI, control: true);
|
|
|
|
|
expect(controller.text, startsWith('***one*** word'));
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
testWidgets('F3 and Shift+F3 cycle search matches', (tester) async {
|
|
|
|
|
await pumpEditor(tester);
|
|
|
|
|
final controller = field(tester).controller! as MarkdownController;
|
|
|
|
|
expect(controller.currentMatch, 0);
|
|
|
|
|
|
|
|
|
|
await _press(tester, LogicalKeyboardKey.f3);
|
|
|
|
|
expect(controller.currentMatch, 1);
|
|
|
|
|
await _press(tester, LogicalKeyboardKey.f3);
|
|
|
|
|
expect(controller.currentMatch, 0);
|
|
|
|
|
await _press(tester, LogicalKeyboardKey.f3, shift: true);
|
|
|
|
|
expect(controller.currentMatch, 1);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
testWidgets('arrows scroll in read and preview mode, not in edit mode',
|
|
|
|
|
(tester) async {
|
|
|
|
|
note.writeAsStringSync(List.generate(200, (i) => 'line $i').join('\n'));
|
|
|
|
|
await pumpEditor(tester);
|
|
|
|
|
ScrollController sourceScroll() => field(tester).scrollController!;
|
|
|
|
|
|
|
|
|
|
await _press(tester, LogicalKeyboardKey.arrowDown);
|
|
|
|
|
expect(sourceScroll().offset, greaterThan(0));
|
|
|
|
|
await _press(tester, LogicalKeyboardKey.arrowUp);
|
|
|
|
|
expect(sourceScroll().offset, 0);
|
|
|
|
|
|
|
|
|
|
await _press(tester, LogicalKeyboardKey.arrowDown);
|
|
|
|
|
final readOffset = sourceScroll().offset;
|
|
|
|
|
await _press(tester, LogicalKeyboardKey.keyE, control: true);
|
|
|
|
|
expect(sourceScroll().offset, readOffset,
|
|
|
|
|
reason: 'entering edit mode keeps the scroll position');
|
|
|
|
|
await _press(tester, LogicalKeyboardKey.arrowDown);
|
|
|
|
|
expect(sourceScroll().offset, readOffset,
|
|
|
|
|
reason: 'arrows move the caret in edit mode');
|
|
|
|
|
|
|
|
|
|
await _press(tester, LogicalKeyboardKey.keyP, control: true);
|
|
|
|
|
final preview = tester
|
|
|
|
|
.widget<SingleChildScrollView>(find.byType(SingleChildScrollView))
|
|
|
|
|
.controller!;
|
|
|
|
|
await _press(tester, LogicalKeyboardKey.arrowDown);
|
|
|
|
|
expect(preview.offset, greaterThan(0));
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
testWidgets('Page Down and Page Up scroll by a screen in read mode',
|
|
|
|
|
(tester) async {
|
|
|
|
|
note.writeAsStringSync(List.generate(400, (i) => 'line $i').join('\n'));
|
|
|
|
|
await pumpEditor(tester);
|
|
|
|
|
final scroll = field(tester).scrollController!;
|
|
|
|
|
|
|
|
|
|
await _press(tester, LogicalKeyboardKey.pageDown);
|
|
|
|
|
final page = scroll.offset;
|
|
|
|
|
expect(page, closeTo(scroll.position.viewportDimension * 0.9, 1));
|
|
|
|
|
await _press(tester, LogicalKeyboardKey.pageUp);
|
|
|
|
|
expect(scroll.offset, 0);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
testWidgets('Ctrl+Q quits, asking first about unsaved changes',
|
|
|
|
|
(tester) async {
|
|
|
|
|
var quits = 0;
|
|
|
|
|
await pumpEditor(tester, onQuit: () async => quits++);
|
|
|
|
|
|
|
|
|
|
await _press(tester, LogicalKeyboardKey.keyQ, control: true);
|
|
|
|
|
expect(quits, 1);
|
|
|
|
|
|
|
|
|
|
field(tester).controller!.text = 'changed';
|
|
|
|
|
await _press(tester, LogicalKeyboardKey.keyQ, control: true);
|
|
|
|
|
expect(find.text('Save Changes?'), findsOneWidget);
|
|
|
|
|
expect(quits, 1);
|
|
|
|
|
|
|
|
|
|
await tester.tap(find.text('Discard'));
|
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
|
expect(quits, 2);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
testWidgets('Ctrl+L 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();
|
|
|
|
|
expect(find.byType(NoteEditorScreen), findsOneWidget);
|
|
|
|
|
|
|
|
|
|
await _press(tester, LogicalKeyboardKey.keyL, control: true);
|
|
|
|
|
expect(find.byType(NoteEditorScreen), findsNothing);
|
|
|
|
|
});
|
|
|
|
|
|
2026-10-13 11:47:01 +03:00
|
|
|
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');
|
|
|
|
|
});
|
|
|
|
|
|
2026-10-13 11:31:14 +03:00
|
|
|
testWidgets('the open note follows theme switches', (tester) async {
|
|
|
|
|
final mode = ValueNotifier(ThemeMode.light);
|
|
|
|
|
await tester.runAsync(() async {
|
|
|
|
|
await tester.pumpWidget(ValueListenableBuilder(
|
|
|
|
|
valueListenable: mode,
|
|
|
|
|
builder: (context, themeMode, _) => MaterialApp(
|
|
|
|
|
theme: ThemeData(brightness: Brightness.light),
|
|
|
|
|
darkTheme: ThemeData(brightness: Brightness.dark),
|
|
|
|
|
themeMode: themeMode,
|
|
|
|
|
home: NoteEditorScreen(note: note, notesDirectoryPath: null),
|
|
|
|
|
),
|
|
|
|
|
));
|
|
|
|
|
await Future<void>.delayed(const Duration(milliseconds: 100));
|
|
|
|
|
});
|
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
|
final controller = field(tester).controller! as MarkdownController;
|
|
|
|
|
expect(controller.theme, same(dayTheme));
|
|
|
|
|
|
|
|
|
|
mode.value = ThemeMode.dark;
|
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
|
expect(controller.theme, same(nightTheme));
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
testWidgets('Ctrl+= and Ctrl+- change the font size', (tester) async {
|
|
|
|
|
await pumpEditor(tester);
|
|
|
|
|
double size() => tester.widget<MarkdownView>(find.byType(MarkdownView)).fontSize;
|
|
|
|
|
|
|
|
|
|
await _press(tester, LogicalKeyboardKey.equal, control: true);
|
|
|
|
|
expect(size(), 18);
|
|
|
|
|
await _press(tester, LogicalKeyboardKey.minus, control: true);
|
|
|
|
|
await _press(tester, LogicalKeyboardKey.minus, control: true);
|
|
|
|
|
expect(size(), 14);
|
|
|
|
|
await _press(tester, LogicalKeyboardKey.digit0, control: true);
|
|
|
|
|
expect(size(), 16);
|
|
|
|
|
});
|
|
|
|
|
}
|