JJ_MKVMaker/test/explorer_keys_drag_test.dart

380 lines
22 KiB
Dart

import 'dart:io';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:jj_mkvmaker/app/settings.dart';
import 'package:jj_mkvmaker/ui/explorer_page.dart';
import 'package:jj_mkvmaker/ui/quick_look.dart';
import 'package:jj_mkvmaker/ui/explorer_drag.dart';
import 'package:jj_mkvmaker/ui/storage_page.dart';
import 'package:path/path.dart' as p;
import 'support/fake_recycle_bin.dart';
import 'package:jj_mkvmaker/platform/windows/recycle_bin.dart' show recycleBinForTest;
import 'support/explorer_harness.dart';
/// E5e: 예전 explorer_test.dart 에서 나눈 탐색기 화면 시험 - 단축키 (TC · Windows 탐색기) · 창 사이 끌어 놓기 · 진행 패널 · 빠른 보기 · 저장 공간 분석 · 열 머리 정렬
void main() {
useExplorerFixture();
testWidgets('83: Total Commander 모양 단축키 - F5 복사 · F7 새 폴더 · Space/Insert 표시 (Space 는 빠른 필터가 아님)', (tester) async {
File(p.join(left, 'e.txt')).writeAsStringSync('e');
c.settings.explorerStyle = 'totalcmd';
await open(tester);
await act(tester, () => tester.tap(find.text('doc')));
await settle(tester, () => false, rounds: 15);
// Insert: 표시하고 다음 줄 (e.txt) 로, Space: 지금 줄 (e.txt) 표시
await act(tester, () => tester.sendKeyEvent(LogicalKeyboardKey.insert));
await calm(tester);
expect(find.textContaining('1개 표시함'), findsOneWidget);
await act(tester, () => tester.sendKeyEvent(LogicalKeyboardKey.space));
await calm(tester);
expect(find.textContaining('2개 표시함'), findsOneWidget, reason: 'Insert 뒤 다음 줄이 표시됨');
expect(find.byIcon(Icons.filter_alt), findsNothing, reason: 'Space 가 빠른 필터를 열지 않는다');
// F5 = 복사 (확인 창) - 취소
await act(tester, () => tester.sendKeyEvent(LogicalKeyboardKey.f5));
await calm(tester);
expect(find.widgetWithText(FilledButton, '복사'), findsOneWidget);
await tester.tap(find.text('취소').last);
await calm(tester);
// F7 = 새 폴더
await act(tester, () => tester.sendKeyEvent(LogicalKeyboardKey.f7));
await calm(tester);
expect(find.text('새 폴더'), findsWidgets);
await tester.tap(find.text('취소').last);
await calm(tester);
});
testWidgets('83 (28 지적): Total Commander 모양 F8 = 지우기 - 확인 창 [취소] 면 그대로, [휴지통으로] 면 휴지통으로', (tester) async {
final bin = recycleBinForTest;
if (bin is! FakeRecycleBin) return markTestSkipped('실제 휴지통 (JJ_TEST_REAL_RECYCLE=1) 에서는 건너뜀');
if (!Platform.isWindows) return markTestSkipped('휴지통은 Windows 만 (Android 는 앱 휴지통 시험)');
final victim = File(p.join(left, 'tcdel.txt'))..writeAsStringSync('x');
c.settings.explorerStyle = 'totalcmd';
await open(tester);
await settle(tester, () => find.text('tcdel').evaluate().isNotEmpty);
await act(tester, () => tester.tap(find.text('tcdel')));
await settle(tester, () => false, rounds: 15);
// F8 → 확인 창 → [취소]: 아무것도 지우지 않음
await act(tester, () => tester.sendKeyEvent(LogicalKeyboardKey.f8));
await calm(tester);
expect(find.text('휴지통으로 보내기'), findsOneWidget);
expect(find.widgetWithText(FilledButton, '휴지통으로'), findsOneWidget, reason: '처음 값은 휴지통 (영구 삭제 아님)');
await tester.tap(find.text('취소').last);
await calm(tester);
expect(victim.existsSync(), isTrue);
expect(bin.items, isEmpty);
// F8 → [휴지통으로]: 휴지통 길로 (영구 삭제가 아님)
await act(tester, () => tester.sendKeyEvent(LogicalKeyboardKey.f8));
await calm(tester);
await act(tester, () => tester.tap(find.widgetWithText(FilledButton, '휴지통으로')));
await settle(tester, () => !victim.existsSync() && find.byType(SnackBar).evaluate().isNotEmpty);
expect(victim.existsSync(), isFalse);
expect(bin.items.keys, [victim.path], reason: '휴지통으로 보냄 (되돌릴 수 있음)');
expect(File(p.join(left, 'doc.txt')).existsSync(), isTrue, reason: '고른 것만');
expect(find.text('되돌리기'), findsOneWidget);
await calm(tester);
});
testWidgets('83: Windows 탐색기 모양 단축키 - Ctrl+A 모두 표시 · Ctrl+C 담기 · Ctrl+V 붙여넣기', (tester) async {
c.settings.explorerStyle = 'windows';
await open(tester);
await act(tester, () => tester.tap(find.text('doc.txt')));
await settle(tester, () => false, rounds: 15);
await act(tester, () async {
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft);
await tester.sendKeyEvent(LogicalKeyboardKey.keyA);
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft);
});
await calm(tester);
expect(find.textContaining('2개 표시함'), findsOneWidget, reason: 'doc.txt · sub');
await act(tester, () async {
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft);
await tester.sendKeyEvent(LogicalKeyboardKey.keyC);
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft);
});
await calm(tester);
expect(find.textContaining('담았습니다'), findsOneWidget);
});
group('82 창 사이 끌어 놓기', () {
test('놓을 수 있는 곳: 같은 창 · 자기 안 · 원본 폴더 자신은 안 됨', () {
final a = p.join('C:', 'x', 'a'), f = p.join('C:', 'x', 'f.txt');
expect(canDropInto(DragLoad(0, [f]), 1, p.join('C:', 'y')), isTrue);
expect(canDropInto(DragLoad(0, [f]), 0, p.join('C:', 'y')), isFalse, reason: '같은 창');
expect(canDropInto(DragLoad(0, [a]), 1, p.join(a, 'in')), isFalse, reason: '자기 안');
expect(canDropInto(DragLoad(0, [f]), 1, p.join('C:', 'x')), isFalse, reason: '원본 폴더 자신');
});
/// 오른쪽 창에 보이는 [name] 줄 (두 창 모두 같은 이름이 보이면 오른쪽 것)
Offset rightOf(WidgetTester tester, String name) {
final all = find.text(name).evaluate().map((e) => tester.getCenter(find.byWidget(e.widget).first)).toList()
..sort((x, y) => x.dx.compareTo(y.dx));
return all.last;
}
Future<void> mouseDrag(WidgetTester tester, Offset from, Offset to) async {
final g = await tester.startGesture(from, kind: PointerDeviceKind.mouse);
await g.moveBy(const Offset(20, 0));
await tester.pump();
await g.moveTo(to);
await tester.pump();
await g.up();
await calm(tester);
}
testWidgets('짧게 움직이면 고르기 · 같은 창에는 놓아도 아무 일 없음 · 묻기 [취소] 도 아무 일 없음', (tester) async {
await open(tester);
final doc = tester.getCenter(find.text('doc.txt').first);
// 4px 만 움직임 = 누르기 (끌기 아님)
final g = await tester.startGesture(doc, kind: PointerDeviceKind.mouse);
await g.moveBy(const Offset(4, 0));
await g.up();
await calm(tester);
expect(find.byType(AlertDialog), findsNothing);
// 같은 창의 sub 위에 놓기 - 못 놓음
await mouseDrag(tester, doc, tester.getCenter(find.text('sub').first));
expect(find.byType(AlertDialog), findsNothing, reason: '같은 창에는 놓지 않는다');
// 오른쪽 창 (right 폴더) 에 놓기 → 묻기 → 취소
await mouseDrag(tester, doc, rightOf(tester, 'right'));
expect(find.text('1개 항목을 놓았습니다'), findsOneWidget);
await tester.tap(find.text('취소').last);
await calm(tester);
expect(File(p.join(left, 'doc.txt')).existsSync(), isTrue);
expect(File(p.join(right, 'doc.txt')).existsSync(), isFalse);
});
testWidgets('이동 → 같은 이름을 덮어쓴 파일은 되돌리지 않는다고 파일로 알림 (15 지적)', (tester) async {
File(p.join(right, 'doc.txt')).writeAsStringSync('old');
File(p.join(left, 'e.txt')).writeAsStringSync('e');
c.settings
..explorerDropAction = 'move'
..copyConflict = 'overwrite';
await open(tester);
// 전체 시험 중 (부하) 에는 목록을 늦게 읽어 e.txt 를 못 찾았다 - 두 파일이 보일 때까지
await settle(tester, () => find.text('e.txt').evaluate().isNotEmpty && find.text('doc.txt').evaluate().isNotEmpty);
await act(tester, () => tester.tap(find.text('선택').first));
await act(tester, () => tester.tap(markOf('doc.txt')));
await act(tester, () => tester.tap(markOf('e.txt')));
await calm(tester);
await mouseDrag(tester, tester.getCenter(find.text('e.txt').first), rightOf(tester, 'right'));
await settle(tester, () => find.widgetWithText(FilledButton, '이동').evaluate().isNotEmpty);
await act(tester, () => tester.tap(find.widgetWithText(FilledButton, '이동')));
await settle(tester, () => find.text('되돌리기').evaluate().isNotEmpty);
expect(find.textContaining('같은 이름을 덮어쓴 파일 1개는 되돌리지 않습니다'), findsOneWidget);
expect(find.textContaining('합친 폴더'), findsNothing);
ScaffoldMessenger.of(tester.element(find.byType(ExplorerPage))).clearSnackBars();
await calm(tester);
await tester.pump(const Duration(seconds: 2)); // 진행 막대가 내려가는 시간 (1.2초) 까지
});
testWidgets('이동 → [되돌리기]: 새로 생긴 것은 통째로, 이미 있던 폴더에 합친 것은 옮긴 파일만', (tester) async {
// 오른쪽에 이미 sub 폴더 (원래 파일 하나) - 합치기
File(p.join(right, 'sub', 'mine.txt'))
..createSync(recursive: true)
..writeAsStringSync('mine');
c.settings
..explorerDropAction = 'move'
..copyConflict = 'overwrite';
await open(tester);
await act(tester, () => tester.tap(find.text('선택').first));
await act(tester, () => tester.tap(markOf('doc.txt')));
await act(tester, () => tester.tap(markOf('sub')));
await calm(tester);
await mouseDrag(tester, tester.getCenter(find.text('doc.txt').first), rightOf(tester, 'right'));
// (놓은 뒤 대상에 이미 있는 이름을 실제 파일로 확인하므로 실제 시간을 흘려 기다린다)
await settle(tester, () => find.widgetWithText(FilledButton, '이동').evaluate().isNotEmpty);
// 지금의 이동 확인 창 그대로
await act(tester, () => tester.tap(find.widgetWithText(FilledButton, '이동')));
await settle(tester, () => find.text('되돌리기').evaluate().isNotEmpty);
expect(File(p.join(right, 'doc.txt')).existsSync(), isTrue);
expect(File(p.join(right, 'sub', 'inner.txt')).existsSync(), isTrue);
expect(Directory(p.join(left, 'sub')).existsSync(), isFalse);
// 되돌리기: 원래 폴더마다 지금의 이동 (확인 창)
await act(tester, () => tester.tap(find.text('되돌리기')));
// 원래 폴더가 둘 (left · left/sub) 이라 이동 확인 창도 둘
for (var i = 0; i < 2; i++) {
await settle(tester, () => find.widgetWithText(FilledButton, '이동').evaluate().isNotEmpty);
await act(tester, () => tester.tap(find.widgetWithText(FilledButton, '이동')));
await settle(tester, () => find.byType(AlertDialog).evaluate().isEmpty, rounds: 50);
}
await settle(tester, () => File(p.join(left, 'doc.txt')).existsSync() && File(p.join(left, 'sub', 'inner.txt')).existsSync());
expect(File(p.join(left, 'doc.txt')).readAsStringSync(), 'hello');
expect(File(p.join(left, 'sub', 'inner.txt')).readAsStringSync(), 'inner');
expect(File(p.join(right, 'sub', 'mine.txt')).readAsStringSync(), 'mine', reason: '원래 있던 폴더 · 파일은 그대로 (폴더째 옮기지 않음)');
expect(File(p.join(right, 'sub', 'inner.txt')).existsSync(), isFalse);
ScaffoldMessenger.of(tester.element(find.byType(ExplorerPage))).clearSnackBars();
await calm(tester);
});
});
testWidgets('85: 진행 패널 [숨기기] → 아래 작은 표시 (누르면 다시) · 끝난 알림의 [폴더 열기]', (tester) async {
await open(tester);
await act(tester, () => tester.tap(find.text('doc.txt')));
await settle(tester, () => false, rounds: 15);
await act(tester, () => tester.tap(find.text('복사').first));
await calm(tester);
await act(tester, () => tester.tap(find.widgetWithText(FilledButton, '복사')));
await settle(tester, () => File(p.join(right, 'doc.txt')).existsSync() && find.text('폴더 열기').evaluate().isNotEmpty);
expect(find.text('폴더 열기'), findsOneWidget, reason: '끝난 알림에 [폴더 열기]');
// 패널은 끝난 뒤 잠깐 남는다 - [숨기기]
expect(find.text('숨기기'), findsOneWidget);
// 패널 · 작은 표시는 끝나고 1.2초 (시험 시계) 뒤 내려간다. pumpAndSettle (calm) 은 애니메이션이 있는 동안 시계를 그만큼
// 넘길 수 있어 (PC 가 바쁠 때 흔들림 - 관리자 지시), 정해진 횟수 대신 조건이 될 때까지 시계를 조금씩만 돌린다
Future<void> until(bool Function() done) async {
for (var i = 0; i < 100 && !done(); i++) {
await tester.runAsync(() => Future<void>.delayed(const Duration(milliseconds: 20)));
await tester.pump(const Duration(milliseconds: 10));
}
}
await act(tester, () => tester.tap(find.text('숨기기')));
await until(() => c.settings.explorerProgressHidden && find.text('복사 · 이동 끝').evaluate().isNotEmpty);
expect(c.settings.explorerProgressHidden, isTrue, reason: '숨김은 기억');
expect(find.text('숨기기'), findsNothing);
expect(find.text('복사 · 이동 끝'), findsOneWidget, reason: '아래 작은 표시');
// 작은 표시를 누름 (아래 알림이 덮고 있어도 - 알림이 닫히기를 기다리면 그사이 작은 표시가 내려갈 수 있다)
tester.widget<TransferProgressChip>(find.byType(TransferProgressChip)).onTap();
await until(() => !c.settings.explorerProgressHidden);
ScaffoldMessenger.of(tester.element(find.byType(ExplorerPage))).clearSnackBars();
expect(c.settings.explorerProgressHidden, isFalse, reason: '누르면 다시 펼침');
ScaffoldMessenger.of(tester.element(find.byType(ExplorerPage))).clearSnackBars();
await calm(tester);
});
testWidgets('81: 빠른 보기 (글 파일 앞부분) - Windows 는 TC 키 F3 (규칙 1), Android 는 Space · 필터에 치는 중에는 그냥 빈칸', (tester) async {
c.settings.explorerStyle = 'windows';
await open(tester);
await act(tester, () => tester.tap(find.text('doc.txt')));
await settle(tester, () => false, rounds: 15);
await act(tester, () => tester.sendKeyEvent(Platform.isWindows ? LogicalKeyboardKey.f3 : LogicalKeyboardKey.space));
await settle(tester, () => find.byType(QuickLook).evaluate().isNotEmpty);
expect(find.byType(QuickLook), findsOneWidget);
await settle(tester, () => find.text('hello').evaluate().isNotEmpty);
expect(find.text('hello'), findsOneWidget, reason: '글 파일 앞부분');
await act(tester, () => tester.sendKeyEvent(LogicalKeyboardKey.escape));
await calm(tester);
expect(find.byType(QuickLook), findsNothing);
// 빠른 필터에 치는 중이면 Space 는 칸의 글자 (빠른 보기를 열지 않음)
await act(tester, () => tester.sendKeyEvent(LogicalKeyboardKey.keyD));
await calm(tester);
expect(find.byType(TextField), findsOneWidget);
await act(tester, () => tester.sendKeyEvent(LogicalKeyboardKey.space));
await calm(tester);
expect(find.byType(QuickLook), findsNothing);
await act(tester, () => tester.sendKeyEvent(LogicalKeyboardKey.escape));
await calm(tester);
// TC: F3
await c.updateSettings((s) => s.explorerStyle = 'totalcmd');
await calm(tester);
await act(tester, () => tester.tap(find.text('doc')));
await settle(tester, () => false, rounds: 15);
await act(tester, () => tester.sendKeyEvent(LogicalKeyboardKey.f3));
await settle(tester, () => find.byType(QuickLook).evaluate().isNotEmpty);
expect(find.byType(QuickLook), findsOneWidget);
await act(tester, () => tester.sendKeyEvent(LogicalKeyboardKey.escape));
await calm(tester);
});
// P0 (15 지적 10/17): 글자 칸에 치는 중에는 파일 단축키가 키를 가로채지 않는다 (예전: 필터 칸의 Delete → 파일 지우기 창)
for (final style in const ['windows', 'xplore', 'totalcmd']) {
testWidgets('P0 $style: 빠른 필터 칸에서 Ctrl+A · C · V · Delete · Space · F8 · Backspace 는 파일에 쓰이지 않는다', (tester) async {
c.settings.explorerStyle = style;
await open(tester);
await act(tester, () => tester.tap(find.text(style == 'totalcmd' ? 'doc' : 'doc.txt')));
await settle(tester, () => false, rounds: 15);
// Ctrl+F 로 필터 칸 열고 글자
Future<void> ctrl(LogicalKeyboardKey k) => act(tester, () async {
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft);
await tester.sendKeyEvent(k);
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft);
});
await ctrl(LogicalKeyboardKey.keyF);
await calm(tester);
final field = find.byType(TextField);
expect(field, findsOneWidget);
await tester.enterText(field, 'do');
await calm(tester);
await ctrl(LogicalKeyboardKey.keyA);
await calm(tester);
final ctl = tester.widget<TextField>(field).controller!;
expect(ctl.selection, const TextSelection(baseOffset: 0, extentOffset: 2), reason: 'Ctrl+A 는 칸의 글 전체 선택');
await ctrl(LogicalKeyboardKey.keyC);
await calm(tester);
await ctrl(LogicalKeyboardKey.keyV);
await calm(tester);
for (final k in [LogicalKeyboardKey.delete, LogicalKeyboardKey.space, LogicalKeyboardKey.f8, LogicalKeyboardKey.backspace]) {
await act(tester, () => tester.sendKeyEvent(k));
await calm(tester);
}
expect(find.textContaining('표시함'), findsNothing, reason: '파일이 표시되지 않음');
expect(find.textContaining('담았습니다'), findsNothing, reason: '파일을 담지 않음');
expect(find.textContaining('휴지통'), findsNothing, reason: '지우기 창이 뜨지 않음');
expect(find.textContaining('지울까요'), findsNothing);
expect(File(p.join(left, 'doc.txt')).existsSync(), isTrue);
expect(field, findsOneWidget, reason: '칸은 그대로 열려 있음 (Backspace 로 위 폴더로 가지 않음)');
// Esc 는 지금처럼 필터를 닫는다
await act(tester, () => tester.sendKeyEvent(LogicalKeyboardKey.escape));
await calm(tester);
expect(find.byType(TextField), findsNothing);
ScaffoldMessenger.of(tester.element(find.byType(ExplorerPage))).clearSnackBars();
await calm(tester);
});
}
testWidgets('87: 폴더 길게 누르기 [저장 공간 분석] → 큰 파일을 누르면 탐색기에서 그 자리로', (tester) async {
File(p.join(left, 'sub', 'big.bin')).writeAsBytesSync(List.filled(4000, 1));
await open(tester);
await tester.longPress(find.text('sub').first);
await calm(tester);
await act(tester, () => tester.tap(find.text('저장 공간 분석')));
await settle(tester, () => find.text('큰 파일').evaluate().isNotEmpty);
await act(tester, () => tester.tap(find.text('큰 파일')));
await calm(tester);
await act(tester, () => tester.tap(find.text('big.bin')));
await settle(tester, () => find.byType(StorageAnalysisPage).evaluate().isEmpty && find.text('big.bin').evaluate().isNotEmpty);
await calm(tester);
expect(find.byType(StorageAnalysisPage), findsNothing);
expect(find.text('big.bin'), findsWidgets, reason: '탐색기에서 그 자리 (sub 안) 가 보인다');
});
testWidgets('84: 열 머리를 누르면 그 기준으로 · 다시 누르면 거꾸로 (처음: 모든 폴더 같은 정렬)', (tester) async {
File(p.join(left, 'a_big.txt')).writeAsStringSync('x' * 50);
c.settings.explorerStyle = 'windows';
await open(tester);
double y(String name) => tester.getTopLeft(find.text(name).first).dy;
expect(y('a_big.txt'), lessThan(y('doc.txt')), reason: '처음: 이름순');
await act(tester, () => tester.tap(find.text('크기').first));
await calm(tester);
expect((c.settings.explorerSort, c.settings.explorerSortDesc), ('size', false));
expect(y('doc.txt'), lessThan(y('a_big.txt')), reason: '크기 오름차순: doc.txt (5) 가 먼저');
await act(tester, () => tester.tap(find.text('크기').first));
await calm(tester);
expect(c.settings.explorerSortDesc, isTrue, reason: '같은 열을 다시 누르면 거꾸로');
// (거꾸로면 트리에 펼쳐진 위 폴더 (Temp 등) 의 차례도 바뀌어 시험 폴더가 화면 밖으로 갈 수 있어 줄 위치는 보지 않는다)
expect(find.byIcon(Icons.arrow_drop_down), findsWidgets, reason: '지금 정렬 열에 ▼');
});
testWidgets('84: "정렬을 폴더마다 기억" 을 켜면 그 폴더에만 (전체 정렬 · 다른 창은 그대로)', (tester) async {
c.settings
..explorerStyle = 'totalcmd'
..explorerSortPerFolder = true;
await open(tester);
// 왼쪽 창에서 날짜 열
await act(tester, () => tester.tap(find.text('날짜').first));
await calm(tester);
expect(c.settings.explorerSort, 'name', reason: '전체 정렬은 그대로');
expect(c.settings.explorerFolderSorts.values, contains('date|0'));
expect(c.settings.explorerFolderSorts.length, 1);
// 저장 · 읽기
expect(AppSettings.fromJson(c.settings.toJson()).explorerFolderSorts, c.settings.explorerFolderSorts);
expect(AppSettings.fromJson(c.settings.toJson()).explorerSortPerFolder, isTrue);
expect(AppSettings.fromJson({'explorerFolderSorts': 'broken'}).explorerFolderSorts, isEmpty, reason: '틀린 값은 비움');
});
}