1601 lines
91 KiB
Dart
1601 lines
91 KiB
Dart
import 'dart:async';
|
||
import 'dart:io';
|
||
|
||
import 'package:flutter/foundation.dart';
|
||
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/app_controller.dart';
|
||
import 'package:jj_mkvmaker/app/settings.dart';
|
||
import 'package:jj_mkvmaker/platform/windows/desktop_storage_service.dart';
|
||
import 'package:jj_mkvmaker/platform/windows/process_media_tool.dart';
|
||
import 'package:jj_mkvmaker/services/app_shell.dart';
|
||
import 'package:jj_mkvmaker/services/platform_services.dart';
|
||
import 'package:jj_mkvmaker/core/file_ops.dart' show formatSize;
|
||
import 'package:jj_mkvmaker/ui/explorer_page.dart';
|
||
import 'package:jj_mkvmaker/ui/explorer_search.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:jj_mkvmaker/ui/path_label.dart';
|
||
import 'package:path/path.dart' as p;
|
||
import 'support/fake_recycle_bin.dart';
|
||
|
||
/// 다른 앱으로 열기를 기록만 하는 셸 (실제로 프로그램을 띄우지 않게)
|
||
class _Shell extends NoopShell {
|
||
final opened = <(String, bool)>[];
|
||
@override
|
||
Future<bool> openWith(String path, {bool choose = false}) async {
|
||
opened.add((path, choose));
|
||
return true;
|
||
}
|
||
}
|
||
|
||
void main() {
|
||
late Directory tmp;
|
||
late String left, right;
|
||
late _Shell shell;
|
||
late AppController c;
|
||
|
||
setUp(() {
|
||
// 휴지통: 기본은 가짜 (이 시험들은 영구 삭제를 고르지만 혹시라도 사용자 휴지통에 닿지 않게)
|
||
useTestRecycleBin();
|
||
tmp = Directory.systemTemp.createTempSync('jj_explorer_');
|
||
left = p.join(tmp.path, 'left');
|
||
right = p.join(tmp.path, 'right');
|
||
File(p.join(left, 'doc.txt'))
|
||
..createSync(recursive: true)
|
||
..writeAsStringSync('hello');
|
||
File(p.join(left, 'sub', 'inner.txt'))
|
||
..createSync(recursive: true)
|
||
..writeAsStringSync('inner');
|
||
Directory(right).createSync();
|
||
shell = _Shell();
|
||
c = AppController(PlatformServices(
|
||
mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService(), shell: shell));
|
||
c.settings.explorerPaths = [left, right];
|
||
});
|
||
// Windows: 방금 닫은 파일 (65 의 잠금 시험) 이나 색인 · 백신이 잠깐 잡고 있으면 지우기가 실패한다 - 될 때까지 기다린다
|
||
tearDown(() async {
|
||
for (var i = 0;; i++) {
|
||
try {
|
||
if (tmp.existsSync()) tmp.deleteSync(recursive: true);
|
||
return;
|
||
} on FileSystemException {
|
||
if (i >= 50) rethrow;
|
||
await Future<void>.delayed(const Duration(milliseconds: 100));
|
||
}
|
||
}
|
||
});
|
||
|
||
Future<void> open(WidgetTester tester) async {
|
||
tester.view.physicalSize = const Size(1600, 900);
|
||
tester.view.devicePixelRatio = 1;
|
||
addTearDown(tester.view.reset);
|
||
await tester.runAsync(() => tester.pumpWidget(MaterialApp(home: ExplorerPage(c: c))));
|
||
await settle(tester, () => find.text('doc.txt').evaluate().isNotEmpty);
|
||
}
|
||
|
||
testWidgets('두 창: 폴더 한 번 = 펼치기 · 파일 한 번 = 선택 · 두 번 = 실행 · 길게 = 메뉴 · 다른 창으로 복사', (tester) async {
|
||
await open(tester);
|
||
// 두 창 + 가운데 버튼 줄. 선택 동그라미: X-plore 스타일 (처음 값) 은 늘 보임 (관리자 결정 10/18 - 설정으로 끌 수 있음)
|
||
expect(find.text('상위 폴더'), findsOneWidget);
|
||
expect(find.text('doc.txt'), findsOneWidget);
|
||
expect(find.byTooltip('표시 (여러 개 고르기)'), findsWidgets);
|
||
// 모니터링 · rsync 는 Rsync 화면에만
|
||
expect(find.text('모니터링'), findsNothing);
|
||
expect(find.text('좌 → 우'), findsNothing);
|
||
|
||
// 폴더를 한 번 누르면 바로 펼친다. 189: 다시 눌러도 (두 번 누름 포함) 접지 않는다 - 접기 · 펼치기는 화살표로
|
||
await act(tester, () => tester.tap(find.text('sub')));
|
||
await settle(tester, () => find.text('inner.txt').evaluate().isNotEmpty);
|
||
expect(find.text('inner.txt'), findsOneWidget);
|
||
await act(tester, () => tester.tap(find.text('sub')));
|
||
await settle(tester, () => false, rounds: 15);
|
||
expect(find.text('inner.txt'), findsOneWidget, reason: '이름을 다시 눌러도 그대로');
|
||
await doubleTap(tester, find.text('sub'));
|
||
await settle(tester, () => false, rounds: 15);
|
||
expect(find.text('inner.txt'), findsOneWidget, reason: '두 번 눌러도 그대로');
|
||
await act(tester, () => tester.tap(find.descendant(
|
||
of: find.ancestor(of: find.text('sub'), matching: find.byType(InkWell)).first, matching: find.byIcon(Icons.expand_more))));
|
||
await settle(tester, () => find.text('inner.txt').evaluate().isEmpty);
|
||
expect(find.text('inner.txt'), findsNothing, reason: '화살표로 접는다');
|
||
// › 화살표로 펼친다
|
||
await act(tester, () => tester.tap(chevronOf('sub')));
|
||
await settle(tester, () => find.text('inner.txt').evaluate().isNotEmpty);
|
||
expect(find.text('inner.txt'), findsOneWidget);
|
||
|
||
// 파일을 한 번 누르면 고르기만 (실행 안 함)
|
||
await act(tester, () => tester.tap(find.text('doc.txt')));
|
||
await settle(tester, () => false, rounds: 15);
|
||
expect(shell.opened, isEmpty);
|
||
// 두 번 누르면 기본 앱으로 실행
|
||
await doubleTap(tester, find.text('doc.txt'));
|
||
await settle(tester, () => shell.opened.isNotEmpty);
|
||
expect(shell.opened.last, (p.join(left, 'doc.txt'), false));
|
||
|
||
// 길게 누르기 → 메뉴 → 다른 앱으로 열기 (고르기 창)
|
||
await tester.longPress(find.text('doc.txt'));
|
||
await calm(tester);
|
||
// 77: 복사 · 이동 · 이름 변경 · 삭제는 메뉴 위 아이콘 줄 (두 창이면 복사는 다른 창으로)
|
||
expect(find.byTooltip('다른 창으로 복사'), findsOneWidget);
|
||
expect(find.text('삭제'), findsWidgets);
|
||
await act(tester, () => tester.tap(find.text('다른 앱으로 열기')));
|
||
await settle(tester, () => shell.opened.length == 2);
|
||
expect(shell.opened.last, (p.join(left, 'doc.txt'), true));
|
||
|
||
// doc.txt 를 고른 채 [복사] → 확인 → 오른쪽 창 폴더에 생긴다
|
||
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.byType(LinearProgressIndicator).evaluate().isEmpty);
|
||
expect(File(p.join(right, 'doc.txt')).readAsStringSync(), 'hello');
|
||
expect(File(p.join(left, 'doc.txt')).existsSync(), isTrue);
|
||
});
|
||
|
||
testWidgets('X-plore a: 한 창 + "숨은 다른 창으로" - 위에 갈 곳 줄 · [복사] 는 그 폴더로 · [지금 폴더로] 로 바꾸기 (처음 값은 담아 두기)', (tester) async {
|
||
expect(AppSettings().explorerOnePaneCopy, 'clip', reason: '처음 값은 지금 그대로');
|
||
c.settings
|
||
..explorerLayout = 'single'
|
||
..explorerOnePaneCopy = 'other';
|
||
await open(tester);
|
||
expect(find.textContaining('복사 · 이동 갈 곳'), findsOneWidget);
|
||
expect(find.textContaining('갈 곳: right'), findsOneWidget, reason: '숨은 창의 폴더 이름 (전체 경로는 풍선 도움말)');
|
||
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.byType(LinearProgressIndicator).evaluate().isEmpty);
|
||
expect(File(p.join(right, 'doc.txt')).readAsStringSync(), 'hello');
|
||
// [지금 폴더로] → 갈 곳이 지금 보는 폴더 (left)
|
||
await act(tester, () => tester.tap(find.text('지금 폴더로')));
|
||
await settle(tester, () => c.settings.explorerPaths[1] == left, rounds: 50);
|
||
expect(c.settings.explorerPaths[1], left);
|
||
ScaffoldMessenger.of(tester.element(find.byType(ExplorerPage))).clearSnackBars();
|
||
await calm(tester);
|
||
await tester.pump(const Duration(seconds: 2));
|
||
});
|
||
|
||
testWidgets('X-plore b: 가운데 버튼 길게 누르기 = 짝 동작 - [복사] 길게 = 이동 창 · [삭제] 길게 = 영구 삭제 창', (tester) async {
|
||
await open(tester);
|
||
await act(tester, () => tester.tap(find.text('doc.txt')));
|
||
await settle(tester, () => false, rounds: 15);
|
||
await tester.longPress(find.text('복사').first);
|
||
await calm(tester);
|
||
expect(find.widgetWithText(FilledButton, '이동'), findsOneWidget, reason: '복사 길게 = 이동');
|
||
await tester.tap(find.text('취소').last);
|
||
await calm(tester);
|
||
await tester.longPress(find.text('삭제').first);
|
||
await calm(tester);
|
||
expect(find.widgetWithText(FilledButton, '영구 삭제'), findsOneWidget, reason: '삭제 길게 = 영구 삭제로 묻기');
|
||
await tester.tap(find.text('취소').last);
|
||
await calm(tester);
|
||
expect(File(p.join(left, 'doc.txt')).existsSync(), isTrue);
|
||
});
|
||
|
||
testWidgets('X-plore d: 설정 "다시 누르면 접기" - 펼친 지금 폴더를 다시 누르면 접힘 · 두 번 누르기의 두 번째는 접지 않음 (처음 값 끔)', (tester) async {
|
||
expect(AppSettings().explorerRetapCollapse, isFalse, reason: '189 그대로');
|
||
c.settings.explorerRetapCollapse = true;
|
||
await open(tester);
|
||
await act(tester, () => tester.tap(find.text('sub')));
|
||
await settle(tester, () => find.text('inner.txt').evaluate().isNotEmpty);
|
||
expect(find.text('inner.txt'), findsOneWidget);
|
||
// 조금 뒤 다시 누름 - 접힘
|
||
await tester.runAsync(() => Future<void>.delayed(const Duration(milliseconds: 800)));
|
||
await act(tester, () => tester.tap(find.text('sub')));
|
||
await settle(tester, () => find.text('inner.txt').evaluate().isEmpty);
|
||
expect(find.text('inner.txt'), findsNothing);
|
||
});
|
||
|
||
testWidgets('X-plore c: X-plore 스타일 트리 맨 위에 다른 저장 장치 · 즐겨찾기 - 누르면 그곳으로, 즐겨찾기는 펼침', (tester) async {
|
||
ExplorerPage.debugVolumes = () => [(left, 'L:'), (right, 'R:')];
|
||
addTearDown(() => ExplorerPage.debugVolumes = null);
|
||
c.settings
|
||
..explorerStyle = 'xplore'
|
||
..folderBookmarks = [FolderBookmark(name: '내 즐겨찾기', path: p.join(left, 'sub'))];
|
||
await open(tester);
|
||
final otherRoot = find.byKey(ValueKey<String>('k1:$right'));
|
||
expect(otherRoot, findsWidgets, reason: '왼쪽 창 트리에 R: (다른 저장 장치) 줄');
|
||
final favs = find.byKey(const ValueKey<String>('k2:\u0000favorites'));
|
||
expect(favs, findsWidgets);
|
||
expect(find.text('내 즐겨찾기'), findsNothing, reason: '접힌 즐겨찾기');
|
||
await act(tester, () => tester.tap(favs.first));
|
||
await calm(tester);
|
||
expect(find.text('내 즐겨찾기'), findsOneWidget);
|
||
await act(tester, () => tester.tap(find.text('내 즐겨찾기')));
|
||
await settle(tester, () => c.settings.explorerPaths.first == p.join(left, 'sub'), rounds: 80);
|
||
expect(c.settings.explorerPaths.first, p.join(left, 'sub'));
|
||
await act(tester, () => tester.tap(find.byKey(ValueKey<String>('k1:$right')).first));
|
||
await settle(tester, () => c.settings.explorerPaths.first == right, rounds: 80);
|
||
expect(c.settings.explorerPaths.first, right, reason: '다른 저장 장치 줄 = 그 저장 장치로');
|
||
// 다른 스타일은 그대로 (맨 위 항목 없음)
|
||
await tester.runAsync(() => c.updateSettings((x) => x.explorerStyle = 'windows'));
|
||
await calm(tester);
|
||
expect(find.byKey(const ValueKey<String>('k2:\u0000favorites')), findsNothing);
|
||
});
|
||
|
||
testWidgets('X-plore ⚖: 폴더 표시 X-plore 식 (1번 폴더 · 2번 안의 파일 · 3번 풀기) - 처음 값은 지금 규칙', (tester) async {
|
||
expect(AppSettings().explorerFolderMarkRule, 'tree', reason: '고르는 범위가 바뀌면 지우기 · 이동 대상이 바뀌어 처음 값은 그대로');
|
||
c.settings.explorerFolderMarkRule = 'xplore';
|
||
await open(tester);
|
||
IconData markIcon(String name) => (tester
|
||
.widget<IconButton>(find.descendant(
|
||
of: find.ancestor(of: find.text(name), matching: find.byType(Material)).first, matching: find.byType(IconButton)))
|
||
.icon as Icon)
|
||
.icon!;
|
||
Future<void> mark() async {
|
||
// 표시 동그라미 (풍선 도움말이 표시 여부에 따라 바뀌므로 줄 안의 단추로 찾는다)
|
||
await act(tester, () => tester.tap(find.descendant(of: find.ancestor(of: find.text('sub'), matching: find.byType(Material)).first, matching: find.byType(IconButton)).first));
|
||
await settle(tester, () => false, rounds: 10);
|
||
}
|
||
|
||
await mark();
|
||
expect((markIcon('sub'), markIcon('inner.txt')), (Icons.check_circle, Icons.check_circle), reason: '1번 = 폴더 자신 (안의 것도 포함)');
|
||
await mark();
|
||
expect((markIcon('sub'), markIcon('inner.txt')), (Icons.check_circle_outline, Icons.check_circle), reason: '2번 = 안의 파일 전체');
|
||
await mark();
|
||
expect((markIcon('sub'), markIcon('inner.txt')), (Icons.check_circle_outline, Icons.check_circle_outline), reason: '3번 = 풀기');
|
||
});
|
||
|
||
testWidgets('X-plore ⚖: 탐색기 안에서 옆으로 밀기 = 창 바꾸기 (한 창이면 숨은 창과 바꿈)', (tester) async {
|
||
c.settings.explorerLayout = 'single';
|
||
await open(tester);
|
||
expect(c.settings.explorerPaths, [left, right]);
|
||
// 화면 가운데 높이에서 (위 · 아래 가장자리 1/5 는 밀기로 보지 않는다 - SwipeNav)
|
||
await tester.flingFrom(tester.getCenter(find.byType(ExplorerPage)), const Offset(-400, 0), 1500);
|
||
await settle(tester, () => c.settings.explorerPaths.first == right, rounds: 60);
|
||
expect(c.settings.explorerPaths, [right, left], reason: '숨은 창이 보이는 창으로');
|
||
ScaffoldMessenger.of(tester.element(find.byType(ExplorerPage))).clearSnackBars();
|
||
await tester.pump(const Duration(seconds: 1));
|
||
});
|
||
|
||
testWidgets('X-plore e: Android 앱 휴지통 - 지우기 = 휴지통으로 · [되돌리기] · [휴지통] 화면에서 되살리기', (tester) async {
|
||
// 저장 장치는 시험 폴더 (실제 C:\ 에 .jj_trash 를 만들지 않게)
|
||
ExplorerPage.debugAndroidTrash = true;
|
||
ExplorerPage.debugVolumes = () => [(tmp.path, 'T:')];
|
||
addTearDown(() {
|
||
ExplorerPage.debugAndroidTrash = null;
|
||
ExplorerPage.debugVolumes = null;
|
||
});
|
||
await open(tester);
|
||
final doc = p.join(left, 'doc.txt');
|
||
await act(tester, () => tester.tap(find.text('doc.txt')));
|
||
await settle(tester, () => false, rounds: 10);
|
||
await act(tester, () => tester.tap(find.text('삭제').first));
|
||
await calm(tester);
|
||
expect(find.textContaining('앱 휴지통에 14일'), findsOneWidget);
|
||
await act(tester, () => tester.tap(find.widgetWithText(FilledButton, '휴지통으로')));
|
||
await settle(tester, () => !File(doc).existsSync() && find.text('되돌리기').evaluate().isNotEmpty);
|
||
expect(File(doc).existsSync(), isFalse);
|
||
expect(Directory(p.join(tmp.path, '.jj_trash')).existsSync(), isTrue);
|
||
// [되돌리기]
|
||
await act(tester, () => tester.tap(find.text('되돌리기')));
|
||
await settle(tester, () => File(doc).existsSync());
|
||
expect(File(doc).readAsStringSync(), 'hello');
|
||
ScaffoldMessenger.of(tester.element(find.byType(ExplorerPage))).clearSnackBars();
|
||
await calm(tester);
|
||
// 다시 지우고 [휴지통] 화면에서 되살리기
|
||
await act(tester, () => tester.tap(find.text('doc.txt')));
|
||
await settle(tester, () => false, rounds: 10);
|
||
await act(tester, () => tester.tap(find.text('삭제').first));
|
||
await calm(tester);
|
||
await act(tester, () => tester.tap(find.widgetWithText(FilledButton, '휴지통으로')));
|
||
await settle(tester, () => !File(doc).existsSync());
|
||
ScaffoldMessenger.of(tester.element(find.byType(ExplorerPage))).clearSnackBars();
|
||
await calm(tester);
|
||
// 휴지통 화면을 연다 (닫힐 때까지 기다리는 Future 라 기다리지 않는다)
|
||
unawaited((tester.state(find.byType(ExplorerPage)) as dynamic).debugOpenTrash() as Future<void>);
|
||
await settle(tester, () => find.byType(CheckboxListTile).evaluate().isNotEmpty);
|
||
await act(tester, () => tester.tap(find.byType(CheckboxListTile).first));
|
||
await calm(tester);
|
||
await act(tester, () => tester.tap(find.text('되살리기')));
|
||
await settle(tester, () => File(doc).existsSync());
|
||
expect(File(doc).readAsStringSync(), 'hello');
|
||
await tester.pageBack();
|
||
await calm(tester);
|
||
ScaffoldMessenger.of(tester.element(find.byType(ExplorerPage))).clearSnackBars();
|
||
await tester.pump(const Duration(seconds: 2));
|
||
});
|
||
|
||
testWidgets('178 보충: 설정의 원본 지우기 옵션은 복사 창에서 빠진 까닭을 알린다 (이동 창에는 없음)', (tester) async {
|
||
c.settings
|
||
..copyMethodFile = 'robocopy'
|
||
..robocopyOptions = '/E /MOV';
|
||
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);
|
||
expect(find.textContaining('방법: robocopy'), findsOneWidget);
|
||
expect(find.text('환경 설정의 /MOV 는 복사에서는 쓰지 않습니다 (이동은 [이동] 으로)'), findsOneWidget);
|
||
await act(tester, () => tester.tap(find.text('취소').last));
|
||
await calm(tester);
|
||
await act(tester, () => tester.tap(find.text('이동').first));
|
||
await calm(tester);
|
||
expect(find.textContaining('옮길까요?'), findsOneWidget);
|
||
expect(find.textContaining('복사에서는 쓰지 않습니다'), findsNothing);
|
||
await act(tester, () => 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);
|
||
}, skip: !Platform.isWindows);
|
||
|
||
testWidgets('바로 실행 모드: 폴더를 누르면 펼치고, 파일을 누르면 바로 실행', (tester) async {
|
||
c.settings.explorerClick = 'open';
|
||
await open(tester);
|
||
await act(tester, () => tester.tap(find.text('sub')));
|
||
await settle(tester, () => find.text('inner.txt').evaluate().isNotEmpty);
|
||
expect(find.text('inner.txt'), findsOneWidget);
|
||
await act(tester, () => tester.tap(find.text('doc.txt')));
|
||
await settle(tester, () => shell.opened.isNotEmpty);
|
||
expect(shell.opened.single, (p.join(left, 'doc.txt'), false));
|
||
});
|
||
|
||
testWidgets('폴더 + 파일 목록 배치 · Windows 탐색기 / Total Commander 스타일', (tester) async {
|
||
c.settings
|
||
..explorerLayout = 'split'
|
||
..explorerStyle = 'windows';
|
||
await open(tester);
|
||
// 왼쪽 트리는 폴더만, 오른쪽 목록은 고른 폴더 (left) 의 내용
|
||
expect(find.text('sub'), findsNWidgets(2)); // 트리 + 목록
|
||
expect(find.text('doc.txt'), findsOneWidget); // 목록에만
|
||
expect(find.text('수정한 날짜'), findsOneWidget); // Windows 탐색기 열 머리
|
||
expect(find.text('TXT 파일'), findsOneWidget);
|
||
expect(find.text('상위 폴더'), findsOneWidget); // 버튼 줄은 두 창 사이
|
||
|
||
// 왼쪽 트리에서 sub 를 고르면 오른쪽 목록이 그 폴더로
|
||
await act(tester, () => tester.tap(find.text('sub').first));
|
||
await settle(tester, () => find.text('inner.txt').evaluate().isNotEmpty);
|
||
expect(find.text('inner.txt'), findsOneWidget);
|
||
expect(find.text('doc.txt'), findsNothing);
|
||
// 목록의 ".." 을 누르면 상위 폴더로
|
||
await act(tester, () => tester.tap(find.text('..')));
|
||
await settle(tester, () => find.text('doc.txt').evaluate().isNotEmpty);
|
||
expect(find.text('doc.txt'), findsOneWidget);
|
||
// 목록의 폴더를 두 번 누르면 들어간다
|
||
await doubleTap(tester, find.text('sub').last);
|
||
await settle(tester, () => find.text('inner.txt').evaluate().isNotEmpty);
|
||
expect(find.text('inner.txt'), findsOneWidget);
|
||
|
||
// 이 배치에도 복사 · 이동: 길게 눌러 [복사] 로 담고 → 다른 폴더를 골라 [붙여넣기]
|
||
await tester.longPress(find.text('inner.txt'));
|
||
await calm(tester);
|
||
expect(find.byTooltip('다른 창으로 복사'), findsNothing);
|
||
await tester.tap(find.text('복사').last);
|
||
await calm(tester);
|
||
await act(tester, () => tester.tap(find.text('..')));
|
||
await settle(tester, () => find.text('doc.txt').evaluate().isNotEmpty);
|
||
// 73 (관리자): 담아 둔 것이 있으면 [붙여넣기] 는 좁아도 [더 보기] 로 가지 않고 줄에 남는다
|
||
expect(find.text('더 보기'), findsOneWidget, reason: '이 너비에서는 줄이 넘친다');
|
||
expect(find.text('붙여넣기'), findsOneWidget);
|
||
await act(tester, () => tester.tap(find.text('붙여넣기')));
|
||
await calm(tester);
|
||
await act(tester, () => tester.tap(find.widgetWithText(FilledButton, '복사')));
|
||
await settle(tester, () => File(p.join(left, 'inner.txt')).existsSync() && find.byType(LinearProgressIndicator).evaluate().isEmpty);
|
||
expect(File(p.join(left, 'inner.txt')).readAsStringSync(), 'inner');
|
||
expect(File(p.join(left, 'sub', 'inner.txt')).existsSync(), isTrue); // 복사이므로 원본은 그대로
|
||
await settle(tester, () => find.text('inner.txt').evaluate().isNotEmpty);
|
||
expect(find.text('inner.txt'), findsOneWidget); // 목록에 바로 보임
|
||
|
||
// Total Commander: [폴더] · <DIR> · 확장자 칸
|
||
await c.updateSettings((s) => s.explorerStyle = 'totalcmd');
|
||
await settle(tester, () => find.text('[sub]').evaluate().length == 2);
|
||
expect(find.text('[sub]'), findsNWidgets(2));
|
||
expect(find.text('<DIR>'), findsOneWidget);
|
||
expect(find.text('doc'), findsOneWidget); // 확장자를 뺀 이름
|
||
expect(find.text('txt'), findsNWidgets(2)); // 확장자 칸 (doc.txt · 붙여넣은 inner.txt)
|
||
expect(find.text('확장자'), findsOneWidget);
|
||
// 알림 (복사 끝 등) 이 시험이 끝난 뒤에 닫히지 않게 정리
|
||
ScaffoldMessenger.of(tester.element(find.byType(ExplorerPage))).clearSnackBars();
|
||
await calm(tester);
|
||
});
|
||
|
||
testWidgets('선택 모드: 파일 누르기 = 선택 · 취소, 폴더 누르기 = 자기 자신과 안의 것 모두 선택 · 다시 누르면 모두 취소', (tester) async {
|
||
File(p.join(left, 'sub', 'second.txt')).writeAsStringSync('2');
|
||
await open(tester);
|
||
await act(tester, () => tester.tap(find.text('선택').first)); // 가운데 [선택] 버튼
|
||
expect(find.byTooltip('표시 (여러 개 고르기)'), findsWidgets); // 동그라미가 보인다
|
||
// 파일: 누르면 선택, 다시 누르면 취소 (실행하지 않음)
|
||
await act(tester, () => tester.tap(find.text('doc.txt')));
|
||
expect(find.textContaining('1개 표시함'), findsOneWidget);
|
||
await act(tester, () => tester.tap(find.text('doc.txt')));
|
||
expect(find.textContaining('0개 표시함'), findsNWidgets(2)); // 두 창 모두 (각 창의 선택 수)
|
||
expect(shell.opened, isEmpty);
|
||
|
||
IconData markIcon(String name) => (tester
|
||
.widget<IconButton>(find.descendant(
|
||
of: find.ancestor(of: find.text(name), matching: find.byType(Material)).first,
|
||
matching: find.byType(IconButton)))
|
||
.icon as Icon)
|
||
.icon!;
|
||
|
||
// 폴더: 누르면 자기 자신과 안의 것 모두 선택 (펼쳐서 ✔ 로 보여 줌)
|
||
await act(tester, () => tester.tap(find.text('sub')));
|
||
await settle(tester, () => find.text('inner.txt').evaluate().isNotEmpty);
|
||
expect(find.textContaining('1개 표시함'), findsOneWidget); // 폴더 하나로 안의 것까지
|
||
expect([markIcon('sub'), markIcon('inner.txt'), markIcon('second.txt')], everyElement(Icons.check_circle));
|
||
// 안의 것 하나를 누르면 그것만 빠지고 나머지는 고른 채로
|
||
await act(tester, () => tester.tap(find.text('inner.txt')));
|
||
await settle(tester, () => false, rounds: 5);
|
||
expect(markIcon('inner.txt'), Icons.check_circle_outline);
|
||
expect(markIcon('second.txt'), Icons.check_circle);
|
||
expect(markIcon('sub'), Icons.check_circle_outline);
|
||
// 폴더를 다시 누르면 다시 전체 선택, 한 번 더 누르면 자기 자신과 안의 것 모두 취소
|
||
await act(tester, () => tester.tap(find.text('sub')));
|
||
await settle(tester, () => false, rounds: 5);
|
||
expect(find.textContaining('1개 표시함'), findsOneWidget);
|
||
expect([markIcon('sub'), markIcon('inner.txt'), markIcon('second.txt')], everyElement(Icons.check_circle));
|
||
await act(tester, () => tester.tap(find.text('sub')));
|
||
await settle(tester, () => false, rounds: 5);
|
||
expect(find.textContaining('0개 표시함'), findsNWidgets(2));
|
||
expect([markIcon('sub'), markIcon('inner.txt'), markIcon('second.txt')], everyElement(Icons.check_circle_outline));
|
||
// [선택 끝] → 표시가 모두 풀린다 (X-plore 스타일은 동그라미 자체는 늘 보임 - 관리자 결정 10/18)
|
||
await act(tester, () => tester.tap(find.text('선택 끝').first)); // 두 창 모두에 있음
|
||
expect(find.byTooltip('표시 지우기'), findsNothing);
|
||
expect(find.byTooltip('표시 (여러 개 고르기)'), findsWidgets);
|
||
// 설정에서 끄면 선택 모드에서만 (예전대로)
|
||
await tester.runAsync(() => c.updateSettings((x) => x.explorerAlwaysMarks = false));
|
||
await settle(tester, () => false, rounds: 5);
|
||
expect(find.byTooltip('표시 (여러 개 고르기)'), findsNothing);
|
||
});
|
||
|
||
testWidgets('65: 여러 개를 지울 때 하나가 실패해도 나머지는 지우고, 못 지운 것을 알린다', (tester) async {
|
||
final locked = File(p.join(left, 'locked.txt'))..writeAsStringSync('l');
|
||
final h = locked.openSync(mode: FileMode.append); // Windows: 열려 있는 파일은 지울 수 없다
|
||
addTearDown(h.closeSync);
|
||
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('locked.txt')));
|
||
await act(tester, () => tester.tap(markOf('sub')));
|
||
await settle(tester, () => find.textContaining('3개 표시함').evaluate().isNotEmpty);
|
||
await act(tester, () => tester.tap(find.text('삭제').first));
|
||
await calm(tester);
|
||
await tester.tap(find.text('휴지통을 거치지 않고 영구 삭제 (Shift+Delete)'));
|
||
await calm(tester);
|
||
await act(tester, () => tester.tap(find.widgetWithText(FilledButton, '영구 삭제')));
|
||
await settle(tester, () => find.textContaining('지우지 못했습니다').evaluate().isNotEmpty);
|
||
expect(find.text('3개 중 1개를 지우지 못했습니다'), findsOneWidget);
|
||
expect(find.text('locked.txt'), findsWidgets);
|
||
expect(File(p.join(left, 'doc.txt')).existsSync(), isFalse);
|
||
expect(Directory(p.join(left, 'sub')).existsSync(), isFalse);
|
||
expect(locked.existsSync(), isTrue);
|
||
await tester.tap(find.widgetWithText(FilledButton, '확인'));
|
||
await calm(tester);
|
||
}, skip: !Platform.isWindows);
|
||
|
||
testWidgets('73: 즐겨찾기 - 지금 폴더 넣기 (이름 고침) · 한 번에 가기 · 빼기 (폴더는 그대로)', (tester) async {
|
||
await open(tester);
|
||
// 창 위의 ★ (왼쪽 창)
|
||
await act(tester, () => tester.tap(find.byTooltip('즐겨찾기').first));
|
||
await calm(tester);
|
||
expect(find.text('아직 없습니다. 아래에서 지금 폴더를 넣으세요.'), findsOneWidget);
|
||
await tester.tap(find.text('지금 폴더 추가'));
|
||
await calm(tester);
|
||
await tester.enterText(find.byType(TextField).last, '왼쪽 폴더');
|
||
await tester.tap(find.widgetWithText(FilledButton, '확인'));
|
||
await calm(tester);
|
||
expect([for (final b in c.settings.folderBookmarks) (b.name, b.path)], [('왼쪽 폴더', left)]);
|
||
expect(find.text('왼쪽 폴더'), findsOneWidget);
|
||
await tester.tap(find.text('닫기'));
|
||
await calm(tester);
|
||
expect(find.byIcon(Icons.star), findsOneWidget, reason: '즐겨찾기한 폴더를 보는 창의 ★ 는 채움');
|
||
|
||
// 오른쪽 폴더를 즐겨찾기에 두고 왼쪽 창에서 한 번에 가기
|
||
await c.updateSettings((x) => x.folderBookmarks = [...x.folderBookmarks, FolderBookmark(name: 'R', path: right)]);
|
||
await act(tester, () => tester.tap(find.byTooltip('즐겨찾기').first));
|
||
await calm(tester);
|
||
await act(tester, () => tester.tap(find.text('R')));
|
||
await settle(tester, () => false, rounds: 20);
|
||
expect(find.byIcon(Icons.star), findsNWidgets(2), reason: '두 창 모두 즐겨찾기한 폴더 (왼쪽 창이 R 로 감)');
|
||
|
||
// 빼기: 즐겨찾기에서만 (폴더는 그대로)
|
||
await act(tester, () => tester.tap(find.byTooltip('즐겨찾기').first));
|
||
await calm(tester);
|
||
await tester.tap(find.byTooltip('즐겨찾기에서 빼기 (폴더는 그대로)').first);
|
||
await calm(tester);
|
||
expect([for (final b in c.settings.folderBookmarks) b.name], ['R']);
|
||
expect(Directory(left).existsSync(), isTrue);
|
||
await tester.tap(find.text('닫기'));
|
||
await calm(tester);
|
||
});
|
||
|
||
testWidgets('73: Rsync 화면 - "왼쪽 + 오른쪽" 짝을 넣고 한 번에 연다', (tester) async {
|
||
tester.view.physicalSize = const Size(1600, 900);
|
||
tester.view.devicePixelRatio = 1;
|
||
addTearDown(tester.view.reset);
|
||
c.settings.rsyncPaths = [left, right];
|
||
await tester.runAsync(() => tester.pumpWidget(MaterialApp(home: ExplorerPage(c: c, rsync: true))));
|
||
await settle(tester, () => find.text('doc.txt').evaluate().isNotEmpty);
|
||
await act(tester, () => tester.tap(find.byTooltip('즐겨찾기').first));
|
||
await calm(tester);
|
||
await tester.tap(find.text('지금 짝 추가'));
|
||
await calm(tester);
|
||
await tester.tap(find.widgetWithText(FilledButton, '확인'));
|
||
await calm(tester);
|
||
final pair = c.settings.folderBookmarks.single;
|
||
expect((pair.path, pair.right, pair.name), (left, right, 'left ⇄ right'));
|
||
await tester.tap(find.text('닫기'));
|
||
await calm(tester);
|
||
// 반대 짝을 넣어 두고 열면 두 창이 함께 바뀐다
|
||
await c.updateSettings((x) => x.folderBookmarks = [...x.folderBookmarks, FolderBookmark(name: '거꾸로', path: right, right: left)]);
|
||
await act(tester, () => tester.tap(find.byTooltip('즐겨찾기').first));
|
||
await calm(tester);
|
||
await act(tester, () => tester.tap(find.text('거꾸로')));
|
||
await settle(tester, () => c.settings.rsyncPaths.length == 2 && c.settings.rsyncPaths[0] == right && c.settings.rsyncPaths[1] == left,
|
||
rounds: 100);
|
||
expect(c.settings.rsyncPaths, [right, left]);
|
||
});
|
||
|
||
testWidgets('74: 고르기 도구 - 개수 · 합계 크기, 반전, 패턴, 같은 확장자, 범위 (Shift · 길게 누르기)', (tester) async {
|
||
for (final n in ['a.srt', 'b.srt', 'c.mkv']) {
|
||
File(p.join(left, n)).writeAsStringSync('12345678'); // 8B
|
||
}
|
||
await open(tester);
|
||
await settle(tester, () => find.text('c.mkv').evaluate().isNotEmpty);
|
||
await act(tester, () => tester.tap(find.text('선택').first));
|
||
await act(tester, () => tester.tap(markOf('a.srt')));
|
||
await settle(tester, () => find.textContaining('1개 표시함').evaluate().isNotEmpty);
|
||
expect(find.text('1개 표시함 · 8B'), findsOneWidget);
|
||
|
||
Future<void> tool(String label) async {
|
||
await tester.tap(find.text('고르기 도구').first);
|
||
await calm(tester);
|
||
await tester.tap(find.textContaining(label).last);
|
||
await calm(tester);
|
||
}
|
||
|
||
// 같은 확장자 모두 (.srt) - 마지막으로 누른 a.srt 기준
|
||
await tool('같은 확장자 모두');
|
||
expect(find.text('2개 표시함 · 16B'), findsOneWidget);
|
||
// 반전: 이 폴더의 나머지 (doc.txt · c.mkv · sub)
|
||
await tool('반전');
|
||
expect(find.textContaining('3개 표시함'), findsOneWidget);
|
||
expect(find.textContaining('폴더 1개 (크기 빼고)'), findsOneWidget);
|
||
// 패턴
|
||
await tester.tap(find.text('표시 지우기').first);
|
||
await tester.pump();
|
||
await tool('패턴으로 고르기');
|
||
await tester.enterText(find.byType(TextField).last, '*.MKV; doc.*');
|
||
await tester.tap(find.widgetWithText(FilledButton, '고르기'));
|
||
await calm(tester);
|
||
expect(find.text('2개 표시함 · 13B'), findsOneWidget);
|
||
|
||
// 범위: a.srt 를 표시한 뒤 Shift + c.mkv → a.srt · b.srt · c.mkv (보이는 순서)
|
||
await tester.tap(find.text('표시 지우기').first);
|
||
await tester.pump();
|
||
await act(tester, () => tester.tap(markOf('a.srt')));
|
||
await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftLeft);
|
||
await act(tester, () => tester.tap(find.text('c.mkv').first));
|
||
await tester.sendKeyUpEvent(LogicalKeyboardKey.shiftLeft);
|
||
await tester.pump();
|
||
expect(find.text('3개 표시함 · 24B'), findsOneWidget);
|
||
|
||
// 폰: 첫 항목을 표시한 뒤 마지막 항목을 길게 눌러 [여기까지 범위 고르기]
|
||
await tester.tap(find.text('표시 지우기').first);
|
||
await tester.pump();
|
||
await act(tester, () => tester.tap(markOf('b.srt')));
|
||
await tester.longPress(find.text('doc.txt').first);
|
||
await calm(tester);
|
||
await tester.tap(find.text('여기까지 범위 고르기'));
|
||
await calm(tester);
|
||
expect(find.textContaining('3개 표시함'), findsOneWidget, reason: 'b.srt · c.mkv · doc.txt');
|
||
});
|
||
|
||
testWidgets('77: 길게 누르기 메뉴 - 위 아이콘 줄 · 여럿이면 "n개 항목" (이름 변경은 하나일 때만) · 경로 복사 · 즐겨찾기에 추가 · 삭제는 표시한 것 모두', (tester) async {
|
||
File(p.join(left, 'b.txt')).writeAsStringSync('b');
|
||
String? clip;
|
||
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.platform, (call) async {
|
||
if (call.method == 'Clipboard.setData') clip = (call.arguments as Map)['text'] as String?;
|
||
return null;
|
||
});
|
||
addTearDown(() => tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.platform, null));
|
||
await open(tester);
|
||
await settle(tester, () => find.text('b.txt').evaluate().isNotEmpty);
|
||
|
||
// 하나: 머리 없음 · 아이콘 줄 · 경로 복사 (실제 경로)
|
||
await tester.longPress(find.text('doc.txt').first);
|
||
await calm(tester);
|
||
expect(find.textContaining('개 항목'), findsNothing);
|
||
for (final t in ['다른 창으로 복사', '다른 창으로 이동', '이름 변경', '삭제']) {
|
||
expect(find.byTooltip(t), findsWidgets, reason: t); // 이름 변경 · 삭제는 아래 버튼 줄에도
|
||
}
|
||
await tester.tap(find.text('경로 복사'));
|
||
await calm(tester);
|
||
expect(clip, p.join(left, 'doc.txt'));
|
||
|
||
// 여럿: "2개 항목" · 이름 변경은 일괄 이름 바꾸기 (78) · 경로 복사는 둘 다
|
||
await act(tester, () => tester.tap(find.text('선택').first));
|
||
await act(tester, () => tester.tap(markOf('doc.txt')));
|
||
await act(tester, () => tester.tap(markOf('b.txt')));
|
||
await tester.longPress(find.text('b.txt').first);
|
||
await calm(tester);
|
||
expect(find.text('2개 항목'), findsOneWidget);
|
||
expect(find.byTooltip('2개 삭제'), findsOneWidget);
|
||
await tester.tap(find.byTooltip('2개 이름 일괄 바꾸기'));
|
||
await calm(tester);
|
||
expect(find.text('이름 일괄 바꾸기 · 2개'), findsOneWidget, reason: '78: 여럿이면 일괄 이름 바꾸기 창');
|
||
await tester.tap(find.text('취소').last);
|
||
await calm(tester);
|
||
await tester.longPress(find.text('b.txt').first);
|
||
await calm(tester);
|
||
await tester.tap(find.text('경로 복사 (2개)'));
|
||
await calm(tester);
|
||
expect(clip!.split('\n'), unorderedEquals([p.join(left, 'doc.txt'), p.join(left, 'b.txt')]));
|
||
|
||
// 삭제 (아이콘 줄) 는 표시한 것 모두 - 확인 창에서 취소
|
||
await tester.longPress(find.text('b.txt').first);
|
||
await calm(tester);
|
||
await tester.tap(find.byTooltip('2개 삭제'));
|
||
await calm(tester);
|
||
expect(find.textContaining('2개 항목을'), findsOneWidget);
|
||
await tester.tap(find.text('취소').last);
|
||
await calm(tester);
|
||
expect(File(p.join(left, 'b.txt')).existsSync(), isTrue);
|
||
// 선택 끝 (아래 알림이 [선택 끝] 을 가릴 수 있어 Esc)
|
||
await tester.sendKeyEvent(LogicalKeyboardKey.escape);
|
||
await calm(tester);
|
||
|
||
// 폴더: 즐겨찾기에 추가 (넣은 뒤에는 메뉴에서 빠짐)
|
||
await tester.longPress(find.text('sub').first);
|
||
await calm(tester);
|
||
await tester.tap(find.text('즐겨찾기에 추가'));
|
||
await calm(tester);
|
||
expect(c.settings.folderBookmarks.map((x) => x.path), contains(p.join(left, 'sub')));
|
||
await tester.longPress(find.text('sub').first);
|
||
await calm(tester);
|
||
expect(find.text('즐겨찾기에 추가'), findsNothing);
|
||
await tester.tapAt(const Offset(5, 5));
|
||
await calm(tester);
|
||
});
|
||
|
||
testWidgets('75: 창마다 뒤로 · 앞으로 - 버튼 · Alt+← / → · 마우스 4 · 5번', (tester) async {
|
||
await open(tester);
|
||
final sub = p.join(left, 'sub');
|
||
String now() => c.settings.explorerPaths.first;
|
||
await act(tester, () => tester.tap(find.text('sub').first));
|
||
await settle(tester, () => find.text('inner.txt').evaluate().isNotEmpty);
|
||
// 뒤로 (버튼): 왼쪽 창은 left 로
|
||
await act(tester, () => tester.tap(find.byTooltip('뒤로 (Alt+←)').first));
|
||
await settle(tester, () => now() == left, rounds: 50);
|
||
expect(now(), left);
|
||
// 앞으로 (버튼)
|
||
await act(tester, () => tester.tap(find.byTooltip('앞으로 (Alt+→)').first));
|
||
await settle(tester, () => now() == sub, rounds: 50);
|
||
expect(now(), sub);
|
||
// Alt+← · Alt+→
|
||
await tester.sendKeyDownEvent(LogicalKeyboardKey.altLeft);
|
||
await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft);
|
||
await tester.sendKeyUpEvent(LogicalKeyboardKey.altLeft);
|
||
await settle(tester, () => now() == left, rounds: 50);
|
||
expect(now(), left);
|
||
// 마우스 5번 (앞으로) · 4번 (뒤로)
|
||
Future<void> mouse(int button) async {
|
||
final g = await tester.createGesture(kind: PointerDeviceKind.mouse, buttons: button);
|
||
await g.addPointer(location: tester.getCenter(find.text('doc.txt').first));
|
||
await g.down(tester.getCenter(find.text('doc.txt').first));
|
||
await g.up();
|
||
await g.removePointer();
|
||
}
|
||
|
||
await mouse(kForwardMouseButton);
|
||
await settle(tester, () => now() == sub, rounds: 50);
|
||
expect(now(), sub);
|
||
await mouse(kBackMouseButton);
|
||
await settle(tester, () => now() == left, rounds: 50);
|
||
expect(now(), left);
|
||
});
|
||
|
||
testWidgets('75: 두 창 - 마우스 4 · 5번은 마우스가 올라간 창에 (그 창이 활성으로)', (tester) async {
|
||
File(p.join(right, 'r1', 'x.txt'))
|
||
..createSync(recursive: true)
|
||
..writeAsStringSync('x');
|
||
await open(tester);
|
||
final r1 = p.join(right, 'r1');
|
||
final sub = p.join(left, 'sub');
|
||
String at(int i) => c.settings.explorerPaths[i];
|
||
await act(tester, () => tester.tap(find.text('r1')));
|
||
await settle(tester, () => at(1) == r1 && find.text('x.txt').evaluate().isNotEmpty, rounds: 50);
|
||
// 왼쪽 창이 활성
|
||
await act(tester, () => tester.tap(find.text('sub')));
|
||
await settle(tester, () => at(0) == sub, rounds: 50);
|
||
// 오른쪽 창 위에서 4번 (뒤로) - 오른쪽만 뒤로
|
||
final g = await tester.createGesture(kind: PointerDeviceKind.mouse, buttons: kBackMouseButton);
|
||
await g.addPointer(location: tester.getCenter(find.text('x.txt')));
|
||
await g.down(tester.getCenter(find.text('x.txt')));
|
||
await g.up();
|
||
await g.removePointer();
|
||
await settle(tester, () => at(1) == right, rounds: 50);
|
||
expect([at(0), at(1)], [sub, right]);
|
||
// 오른쪽 창이 활성이 되었으므로 Alt+→ 도 오른쪽
|
||
await tester.sendKeyDownEvent(LogicalKeyboardKey.altLeft);
|
||
await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
|
||
await tester.sendKeyUpEvent(LogicalKeyboardKey.altLeft);
|
||
await settle(tester, () => at(1) == r1, rounds: 50);
|
||
expect([at(0), at(1)], [sub, r1]);
|
||
});
|
||
|
||
testWidgets('75: 지워진 폴더로 앞으로 - 그 항목은 버리고 지금 폴더 그대로 · 이유를 알림', (tester) async {
|
||
await open(tester);
|
||
final sub = p.join(left, 'sub');
|
||
String now() => c.settings.explorerPaths.first;
|
||
await act(tester, () => tester.tap(find.text('sub')));
|
||
await settle(tester, () => now() == sub, rounds: 50);
|
||
await act(tester, () => tester.tap(find.byTooltip('뒤로 (Alt+←)').first));
|
||
await settle(tester, () => now() == left, rounds: 50);
|
||
await tester.runAsync(() => Directory(sub).delete(recursive: true));
|
||
expect(find.text('sub'), findsOneWidget, reason: '밖에서 지운 것은 아직 모름');
|
||
await act(tester, () => tester.tap(find.byTooltip('앞으로 (Alt+→)').first));
|
||
await settle(tester, () => find.textContaining('내역에서 뺐습니다').evaluate().isNotEmpty, rounds: 50);
|
||
expect(find.textContaining('내역에서 뺐습니다'), findsOneWidget);
|
||
expect(now(), left);
|
||
await settle(tester, () => find.text('sub').evaluate().isEmpty, rounds: 50);
|
||
expect(find.text('sub'), findsNothing, reason: '트리에도 남지 않음 (상위 폴더를 다시 읽음)');
|
||
// 버린 항목은 다시 가지 않는다 (앞으로 버튼이 꺼짐)
|
||
final fwd = tester.widget<IconButton>(find.ancestor(of: find.byTooltip('앞으로 (Alt+→)').first, matching: find.byType(IconButton)).first);
|
||
expect(fwd.onPressed, isNull);
|
||
ScaffoldMessenger.of(tester.element(find.byType(Scaffold).first)).clearSnackBars();
|
||
await tester.pump();
|
||
});
|
||
|
||
testWidgets('189: Android 는 트리 화살표를 손가락으로 누를 수 있게 넓게 (줄 높이 전체 · 폭 44)', (tester) async {
|
||
debugDefaultTargetPlatformOverride = TargetPlatform.android;
|
||
addTearDown(() => debugDefaultTargetPlatformOverride = null);
|
||
await open(tester);
|
||
final area = find.ancestor(of: find.descendant(
|
||
of: find.ancestor(of: find.text('sub'), matching: find.byType(InkWell)).first, matching: find.byIcon(Icons.chevron_right)),
|
||
matching: find.byType(InkResponse)).first;
|
||
final size = tester.getSize(area);
|
||
expect(size.width, greaterThanOrEqualTo(44));
|
||
expect(size.height, greaterThanOrEqualTo(34));
|
||
// 아이콘이 아니라 누르는 영역의 가장자리를 눌러도 펼친다
|
||
final r = tester.getRect(area);
|
||
await act(tester, () => tester.tapAt(Offset(r.left + 3, r.top + 3)));
|
||
await settle(tester, () => find.text('inner.txt').evaluate().isNotEmpty);
|
||
expect(find.text('inner.txt'), findsOneWidget);
|
||
debugDefaultTargetPlatformOverride = null;
|
||
});
|
||
|
||
test('203: 크기 · 바뀐 날을 모르면 (잠긴 시스템 파일) "-1B · 1970-01-01" 대신 "—"', () {
|
||
expect(formatSize(-1), '—');
|
||
expect(formatSize(0), '0B');
|
||
expect(fileDateText(DateTime(1970)), '—');
|
||
expect(fileDateText(DateTime.fromMillisecondsSinceEpoch(0)), '—');
|
||
expect(fileDateText(DateTime(2026, 10, 11, 9, 5)), '2026-10-11 09:05');
|
||
});
|
||
|
||
testWidgets('76: 빠른 필터 - Ctrl+F · 글자 바로 치기 · 패턴, 모두 선택은 보이는 것만, Esc 로 닫으면 모두', (tester) async {
|
||
for (final n in ['ep01.mkv', 'ep02.mkv', 'ep10.srt', 'other.txt']) {
|
||
File(p.join(left, n)).writeAsStringSync('12345678');
|
||
}
|
||
await open(tester);
|
||
await settle(tester, () => find.text('other.txt').evaluate().isNotEmpty);
|
||
Iterable<String> names() => [
|
||
for (final n in ['doc.txt', 'sub', 'ep01.mkv', 'ep02.mkv', 'ep10.srt', 'other.txt'])
|
||
if (find.text(n).evaluate().isNotEmpty) n,
|
||
];
|
||
|
||
// Ctrl+F → 칸 · "ep0"
|
||
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft);
|
||
await tester.sendKeyEvent(LogicalKeyboardKey.keyF);
|
||
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft);
|
||
await calm(tester);
|
||
expect(find.textContaining('이름에 든 글자로 좁히기'), findsOneWidget);
|
||
await tester.enterText(find.byType(TextField).first, 'EP0');
|
||
await tester.pump();
|
||
expect(names(), ['ep01.mkv', 'ep02.mkv'], reason: '대소문자 무시');
|
||
expect(find.text('2개 보임'), findsOneWidget);
|
||
|
||
// 모두 선택 · 반전은 보이는 것만
|
||
await act(tester, () => tester.tap(find.text('선택').first));
|
||
await tester.tap(find.text('모두 선택').first);
|
||
await tester.pump();
|
||
expect(find.textContaining('2개 표시함'), findsOneWidget);
|
||
await tester.tap(find.text('선택 끝').first);
|
||
await tester.pump();
|
||
|
||
// 패턴 · 맞는 것 없음
|
||
await tester.enterText(find.byType(TextField).first, '*.srt');
|
||
await tester.pump();
|
||
expect(names(), ['ep10.srt']);
|
||
await tester.enterText(find.byType(TextField).first, 'zzz');
|
||
await tester.pump();
|
||
expect(find.text('맞는 것이 없습니다'), findsOneWidget);
|
||
|
||
// Esc → 닫고 모두
|
||
await tester.sendKeyEvent(LogicalKeyboardKey.escape);
|
||
await calm(tester);
|
||
expect(find.textContaining('이름에 든 글자로 좁히기'), findsNothing);
|
||
expect(names(), containsAll(['doc.txt', 'sub', 'ep01.mkv', 'other.txt']));
|
||
|
||
// 닫으면 키는 탐색기로 돌아온다 - 목록에서 글자를 바로 치면 그 글자로 열린다.
|
||
// 사람처럼 줄을 누르고 곧바로 친다 (두 번 누르기를 기다리는 중에 줄 목록이 바뀜)
|
||
await tester.pump();
|
||
await tester.tap(find.text('doc.txt').first);
|
||
await tester.pump(const Duration(milliseconds: 20));
|
||
await tester.sendKeyEvent(LogicalKeyboardKey.keyO, character: 'o');
|
||
await calm(tester);
|
||
expect(tester.widget<TextField>(find.byType(TextField).first).controller!.text, 'o');
|
||
expect(names(), ['doc.txt', 'other.txt']);
|
||
// 칸에서 더 치면 칸이 받는다 (두 번 들어가지 않음)
|
||
await tester.enterText(find.byType(TextField).first, 'oth');
|
||
await tester.pump();
|
||
expect(names(), ['other.txt']);
|
||
expect(tester.takeException(), isNull);
|
||
});
|
||
|
||
testWidgets('76: 파일 줄을 누르고 곧바로 쳐서 그 줄이 사라지거나 다른 줄이 그 자리에 와도, 누른 것이 다른 항목에 들어가지 않는다', (tester) async {
|
||
for (final n in ['a1.mkv', 'b2.mkv']) {
|
||
File(p.join(left, n)).writeAsStringSync('x');
|
||
}
|
||
Directory(p.join(left, 'a_folder')).createSync();
|
||
await open(tester);
|
||
await settle(tester, () => find.text('b2.mkv').evaluate().isNotEmpty);
|
||
// 누르기 = 선택 (두 번 = 실행) 모드: 파일 줄은 두 번 누르기를 기다린다
|
||
await tester.tap(find.text('b2.mkv').first);
|
||
await tester.pump(const Duration(milliseconds: 20));
|
||
// "a" → b2.mkv 가 사라지고 그 자리에 폴더 · 다른 파일이 온다
|
||
await tester.sendKeyEvent(LogicalKeyboardKey.keyA, character: 'a');
|
||
await tester.pump();
|
||
expect(tester.takeException(), isNull);
|
||
await tester.pump(const Duration(milliseconds: 500));
|
||
expect(tester.takeException(), isNull);
|
||
expect(find.text('b2.mkv'), findsNothing);
|
||
expect(shell.opened, isEmpty, reason: '누른 것이 실행되지 않음');
|
||
// 다른 항목이 고른 것 (강조) 이 되지 않음 - 폴더가 펼쳐지지도 않음
|
||
expect(find.text('a_folder'), findsOneWidget);
|
||
await tester.sendKeyEvent(LogicalKeyboardKey.escape);
|
||
await calm(tester);
|
||
expect(find.text('b2.mkv'), findsOneWidget);
|
||
});
|
||
|
||
test('76: 빠른 필터 맞추기 - 빈 칸은 모두 · 글자는 이름 안 어디든 · * ? 는 이름 전체 패턴', () {
|
||
expect(quickFilterMatch('Show.S01E02.mkv', ''), isTrue);
|
||
expect(quickFilterMatch('Show.S01E02.mkv', 's01e'), isTrue);
|
||
expect(quickFilterMatch('Show.S01E02.mkv', 'e03'), isFalse);
|
||
expect(quickFilterMatch('Show.S01E02.mkv', '*.mkv'), isTrue);
|
||
expect(quickFilterMatch('Show.S01E02.mkv', 'S01*'), isFalse, reason: '패턴은 이름 전체');
|
||
});
|
||
|
||
test('74: 패턴 (* · ? · 여러 개 · 대소문자 무시)', () {
|
||
final re = globToRegExp('*.srt; ep??.mkv');
|
||
expect(['a.srt', 'A.SRT', 'ep01.mkv'].every(re.hasMatch), isTrue);
|
||
expect(['a.srt.bak', 'ep1.mkv', 'x.ass'].any(re.hasMatch), isFalse);
|
||
});
|
||
|
||
test('73: 즐겨찾기 저장 · 읽기 (Rsync 짝 포함)', () {
|
||
final s = AppSettings()
|
||
..folderBookmarks = const [FolderBookmark(name: 'NAS/영화', path: r'\\nas\movies'), FolderBookmark(name: '백업', path: r'D:\a', right: r'E:\b')];
|
||
final b = AppSettings.fromJson(s.toJson()).folderBookmarks;
|
||
expect([for (final x in b) (x.name, x.path, x.right, x.isPair)], [('NAS/영화', r'\\nas\movies', null, false), ('백업', r'D:\a', r'E:\b', true)]);
|
||
});
|
||
|
||
testWidgets('여러 개 표시 → 삭제 · 새 폴더 · 이름 변경', (tester) async {
|
||
await open(tester);
|
||
await act(tester, () => tester.tap(find.text('선택').first));
|
||
// 동그라미로 doc.txt · sub 표시 (반드시 이름으로 찾은 줄의 버튼만: 목록에는 시험 폴더 위쪽의 실제 폴더도 보인다)
|
||
await act(tester, () => tester.tap(markOf('sub')));
|
||
await settle(tester, () => find.text('inner.txt').evaluate().isNotEmpty); // 폴더를 고르면 펼쳐서 안의 것도 ✔
|
||
await act(tester, () => tester.tap(markOf('doc.txt')));
|
||
await settle(tester, () => find.textContaining('2개 표시함').evaluate().isNotEmpty);
|
||
expect(find.textContaining('2개 표시함'), findsOneWidget);
|
||
await act(tester, () => tester.tap(find.text('삭제').first));
|
||
await calm(tester);
|
||
// 지울 항목 이름이 확인 창에 보인다. 65: Windows 는 기본이 휴지통 (시험에서는 영구 삭제를 골라 휴지통을 어지럽히지 않음)
|
||
expect(find.textContaining('2개 항목을 휴지통으로 보낼까요?'), findsOneWidget);
|
||
expect(find.widgetWithText(FilledButton, '휴지통으로'), findsOneWidget);
|
||
expect(find.textContaining('· sub'), findsOneWidget);
|
||
expect(find.textContaining('· doc.txt'), findsOneWidget);
|
||
await tester.tap(find.text('휴지통을 거치지 않고 영구 삭제 (Shift+Delete)'));
|
||
await calm(tester);
|
||
expect(find.textContaining('2개 항목을 지울까요? 되돌릴 수 없습니다.'), findsOneWidget);
|
||
await act(tester, () => tester.tap(find.widgetWithText(FilledButton, '영구 삭제')));
|
||
await settle(tester, () => find.text('doc.txt').evaluate().isEmpty);
|
||
expect(Directory(left).listSync(), isEmpty);
|
||
|
||
// 새 폴더 → 이름 변경 (지운 뒤 창들이 목록을 다시 읽는 동안은 진행 막대가 돌아 pumpAndSettle 이 끝나지 않는다 - 무거울 때 흔들림)
|
||
await idle(tester);
|
||
await act(tester, () => tester.tap(find.text('새 폴더').first));
|
||
await calm(tester);
|
||
await act(tester, () => tester.enterText(find.byType(TextField), 'films'));
|
||
await act(tester, () => tester.tap(find.widgetWithText(FilledButton, '확인')));
|
||
await settle(tester, () => find.text('films').evaluate().isNotEmpty);
|
||
expect(Directory(p.join(left, 'films')).existsSync(), isTrue);
|
||
await idle(tester);
|
||
await act(tester, () => tester.tap(find.text('이름 변경').first));
|
||
await calm(tester);
|
||
await act(tester, () => tester.enterText(find.byType(TextField), 'movies'));
|
||
await act(tester, () => tester.tap(find.widgetWithText(FilledButton, '확인')));
|
||
await settle(tester, () => find.text('movies').evaluate().isNotEmpty);
|
||
expect(Directory(p.join(left, 'movies')).existsSync(), isTrue);
|
||
});
|
||
|
||
testWidgets('창 배치: 한 창 · 버튼 줄 숨김, 버튼 구성: 버튼 빼기 → 설정에 저장', (tester) async {
|
||
await open(tester);
|
||
await tester.tap(find.byTooltip('창 배치 · 버튼 구성'));
|
||
await calm(tester);
|
||
await tester.tap(find.text('창 배치').last);
|
||
await calm(tester);
|
||
await tester.tap(find.widgetWithText(ChoiceChip, '한 창'));
|
||
await tester.pump();
|
||
await tester.tap(find.widgetWithText(ChoiceChip, '숨김'));
|
||
await tester.pump();
|
||
await act(tester, () => tester.tap(find.widgetWithText(FilledButton, '확인')));
|
||
await calm(tester);
|
||
expect([c.settings.explorerLayout, c.settings.explorerToolbar], ['single', 'hidden']);
|
||
expect(find.text('상위 폴더'), findsNothing); // 버튼 줄 숨김
|
||
expect(find.text('doc.txt'), findsOneWidget); // 한 창 (왼쪽만)
|
||
|
||
// ⋮ 메뉴에서 버튼 구성: "찾기" 빼기
|
||
await c.updateSettings((s) => s.explorerToolbar = 'middle');
|
||
await calm(tester);
|
||
await tester.tap(find.byTooltip('창 배치 · 버튼 구성'));
|
||
await calm(tester);
|
||
await tester.tap(find.text('버튼 구성').last);
|
||
await calm(tester);
|
||
// 처음 값 순서 (자주 쓰는 것부터) 에서 뒤쪽이라 목록을 내려 보이게 한 뒤
|
||
await tester.scrollUntilVisible(find.widgetWithText(CheckboxListTile, 'MKV 목록에 추가'), 80,
|
||
scrollable: find.descendant(of: find.byType(AlertDialog), matching: find.byType(Scrollable)).first);
|
||
await tester.tap(find.widgetWithText(CheckboxListTile, 'MKV 목록에 추가'));
|
||
await tester.pump();
|
||
await tester.tap(find.widgetWithText(FilledButton, '확인'));
|
||
await calm(tester);
|
||
expect(c.settings.explorerButtons, isNot(contains('addMkv')));
|
||
expect(c.settings.explorerButtons, contains('copy'));
|
||
expect(find.text('MKV 목록에 추가'), findsNothing);
|
||
|
||
// 설정 저장 · 읽기
|
||
final back = AppSettings.fromJson(c.settings.toJson());
|
||
expect([back.explorerLayout, back.explorerToolbar, back.explorerButtons],
|
||
[c.settings.explorerLayout, c.settings.explorerToolbar, c.settings.explorerButtons]);
|
||
});
|
||
|
||
testWidgets('Rsync 화면: 늘 좌우 두 창 · 폴더만 · 창마다 하나만 고르기 · → ← ⇄ · 모니터링', (tester) async {
|
||
Directory(p.join(left, 'sub2')).createSync();
|
||
Directory(p.join(right, 'rsub')).createSync();
|
||
File(p.join(right, 'r.txt')).writeAsStringSync('r');
|
||
File(p.join(right, 'rsub', 'back.txt')).writeAsStringSync('b'); // ⇄ 비교에서 ← 로 건너갈 것
|
||
c.settings
|
||
..explorerLayout = 'single' // 파일 탐색기 배치와 상관없이 두 창
|
||
..rsyncPaths = [left, right];
|
||
tester.view.physicalSize = const Size(1600, 900);
|
||
tester.view.devicePixelRatio = 1;
|
||
addTearDown(tester.view.reset);
|
||
await tester.runAsync(() => tester.pumpWidget(MaterialApp(home: ExplorerPage(c: c, rsync: true))));
|
||
await settle(tester, () => find.text('sub').evaluate().isNotEmpty && find.text('rsub').evaluate().isNotEmpty);
|
||
expect(find.text('Rsync'), findsWidgets);
|
||
// 폴더만 (파일은 안 보임) · 버튼은 → ← ⇄ 모니터링 (파일 기능 · 선택 없음)
|
||
expect(find.text('doc.txt'), findsNothing);
|
||
expect(find.text('r.txt'), findsNothing);
|
||
for (final t in ['좌 → 우', '좌 ← 우', '좌 ⇄ 우', '모니터링']) {
|
||
expect(find.text(t), findsOneWidget);
|
||
}
|
||
expect(find.text('선택'), findsNothing);
|
||
expect(find.text('삭제'), findsNothing);
|
||
|
||
// 고르지 않고 → : 알림
|
||
await act(tester, () => tester.tap(find.text('좌 → 우')));
|
||
expect(find.text('왼쪽 · 오른쪽 창에서 폴더를 하나씩 고르세요.'), findsOneWidget);
|
||
|
||
// 창마다 하나만: sub → sub2 로 바꾸면 sub 는 풀림, 다시 누르면 취소
|
||
await act(tester, () => tester.tap(find.text('sub')));
|
||
await act(tester, () => tester.tap(find.text('sub2')));
|
||
await settle(tester, () => false, rounds: 5);
|
||
expect(find.byTooltip('고르기 취소'), findsOneWidget);
|
||
await act(tester, () => tester.tap(find.text('sub2')));
|
||
await settle(tester, () => false, rounds: 5);
|
||
expect(find.byTooltip('고르기 취소'), findsNothing);
|
||
await act(tester, () => tester.tap(find.text('sub')));
|
||
await act(tester, () => tester.tap(find.text('rsub')));
|
||
await settle(tester, () => false, rounds: 5);
|
||
expect(find.byTooltip('고르기 취소'), findsNWidgets(2)); // 왼쪽 하나 · 오른쪽 하나
|
||
|
||
// ⇄ : 두 방향을 보여 주고 -u, 취소하면 모니터링에 남지 않는다
|
||
await act(tester, () => tester.tap(find.text('좌 ⇄ 우')));
|
||
await settle(tester, () => find.textContaining('비교하는 중').evaluate().isEmpty, rounds: 400);
|
||
// 72: 실행 전에 무엇이 바뀌는지 (⇄: 양쪽에만 있는 파일이 서로 건너감)
|
||
expect(find.textContaining('→ 새로'), findsOneWidget);
|
||
expect(find.textContaining('← 새로'), findsOneWidget);
|
||
expect(find.text('rsync 양쪽 (⇄)'), findsOneWidget);
|
||
// 109: 짧은 경로 (끝 폴더) + 전체 경로는 작게
|
||
final ls = p.join(left, 'sub'), rs = p.join(right, 'rsub');
|
||
expect(find.text('→ ${shortPath(rs)}'), findsOneWidget);
|
||
expect(find.text('→ ${shortPath(ls)}'), findsOneWidget);
|
||
expect(find.text('$ls/ → $rs/'), findsOneWidget);
|
||
expect(find.text('$rs/ → $ls/'), findsOneWidget);
|
||
expect(find.textContaining('-avPog -u'), findsNWidgets(2));
|
||
await tester.tap(find.text('취소'));
|
||
await calm(tester);
|
||
expect(c.settings.copyTasks, isEmpty);
|
||
expect(find.text('원본 파일 지우기 (--remove-source-files)'), findsNothing); // 양쪽에는 없음
|
||
// → 는 한 방향 · -u 없이 · 원본 파일 지우기를 고를 수 있다 (고르면 원본 폴더 남김 / 지움)
|
||
await act(tester, () => tester.tap(find.text('좌 → 우')));
|
||
await settle(tester, () => find.textContaining('비교하는 중').evaluate().isEmpty, rounds: 400);
|
||
expect(find.text('$ls/ → $rs/'), findsOneWidget);
|
||
expect(find.text('$rs/ → $ls/'), findsNothing);
|
||
expect(find.textContaining('-u'), findsNothing);
|
||
expect(find.text('빈 폴더 지움 · 원본 폴더는 남김'), findsNothing);
|
||
await tester.tap(find.text('원본 파일 지우기 (--remove-source-files)'));
|
||
await calm(tester);
|
||
expect(find.textContaining('-avPog --remove-source-files'), findsOneWidget);
|
||
expect(find.text('빈 폴더 지움 · 원본 폴더는 남김'), findsOneWidget);
|
||
expect(find.text('빈 폴더 지움 · 원본 폴더도 지움'), findsOneWidget);
|
||
expect(find.widgetWithText(FilledButton, '이동'), findsOneWidget);
|
||
await tester.tap(find.text('취소'));
|
||
await calm(tester);
|
||
// 고른 폴더는 기억 (다음에 열 때 그 폴더부터)
|
||
expect(c.settings.rsyncPaths, [p.join(left, 'sub'), p.join(right, 'rsub')]);
|
||
|
||
// 새 폴더: 만든 폴더가 바로 그 창의 원본 · 대상으로 골라진다 (오른쪽 창 rsub 안에)
|
||
await act(tester, () => tester.tap(find.text('rsub')));
|
||
await settle(tester, () => false, rounds: 5); // rsub 를 다시 눌러 취소
|
||
await act(tester, () => tester.tap(find.text('rsub')));
|
||
await settle(tester, () => false, rounds: 5); // 다시 골라 지금 폴더로
|
||
await act(tester, () => tester.tap(find.text('새 폴더')));
|
||
await calm(tester);
|
||
await tester.enterText(find.byType(TextField), '새로만든');
|
||
await act(tester, () => tester.tap(find.widgetWithText(FilledButton, '확인')));
|
||
// 만든 폴더가 보이고 골라질 때까지 (전체 시험 중 바쁠 때는 고르기가 조금 늦다)
|
||
Finder newRow() => find.ancestor(of: find.text('새로만든'), matching: find.byType(Material)).first;
|
||
await settle(tester, () =>
|
||
find.text('새로만든').evaluate().isNotEmpty &&
|
||
find.descendant(of: newRow(), matching: find.byTooltip('고르기 취소')).evaluate().isNotEmpty);
|
||
expect(Directory(p.join(right, 'rsub', '새로만든')).existsSync(), isTrue);
|
||
expect(find.descendant(of: newRow(), matching: find.byTooltip('고르기 취소')), findsOneWidget);
|
||
expect(find.descendant(of: find.ancestor(of: find.text('rsub'), matching: find.byType(Material)).first,
|
||
matching: find.byTooltip('고르기 취소')), findsNothing); // 창마다 하나만
|
||
});
|
||
|
||
testWidgets('78: 여럿을 골라 [이름 변경] = 일괄 이름 바꾸기 → 목록 차례대로 번호 · [되돌리기]', (tester) async {
|
||
File(p.join(left, 'b.txt')).writeAsStringSync('b');
|
||
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('b.txt')));
|
||
await act(tester, () => tester.tap(find.text('이름 변경').first));
|
||
await settle(tester, () => find.text('바뀌는 것 0개').evaluate().isNotEmpty);
|
||
await tester.enterText(find.widgetWithText(TextField, '이름 틀'), 'Ep[C]');
|
||
await tester.pump();
|
||
await act(tester, () => tester.tap(find.widgetWithText(FilledButton, '바꾸기 (2개)')));
|
||
// 알림은 목록을 다시 읽은 뒤에 뜬다 - 알림까지 기다린다 (PC 가 바쁠 때 한 번 흔들림)
|
||
await settle(tester, () => File(p.join(left, 'Ep02.txt')).existsSync() && find.text('2개의 이름을 바꿨습니다.').evaluate().isNotEmpty);
|
||
// 목록 차례 (이름순: b.txt, doc.txt) 대로 번호
|
||
expect(File(p.join(left, 'Ep01.txt')).readAsStringSync(), 'b');
|
||
expect(File(p.join(left, 'Ep02.txt')).readAsStringSync(), 'hello');
|
||
expect(find.text('2개의 이름을 바꿨습니다.'), findsOneWidget);
|
||
await act(tester, () => tester.tap(find.text('되돌리기')));
|
||
// 알림이 닫히는 움직임을 끝까지 (시험이 끝난 뒤에 끝나면 Scaffold 'mounted' 단언에 걸린다)
|
||
await tester.pump(const Duration(seconds: 1));
|
||
await settle(tester, () => File(p.join(left, 'doc.txt')).existsSync() && find.text('doc.txt').evaluate().isNotEmpty);
|
||
expect(File(p.join(left, 'b.txt')).readAsStringSync(), 'b');
|
||
expect(File(p.join(left, 'Ep01.txt')).existsSync(), isFalse);
|
||
});
|
||
|
||
// 79-c (관리자 판단 · 데이터): 찾기 결과에서 폴더와 그 안의 파일을 함께 고르면 안의 것은 한 번만 (폴더와 함께)
|
||
group('79-c 폴더 + 안의 파일을 함께 고름', () {
|
||
/// "sub" 로 찾아 sub 폴더 · sub/sub_a.txt 를 모두 고른다
|
||
Future<void> findSubAll(WidgetTester tester) async {
|
||
File(p.join(left, 'sub', 'sub_a.txt')).writeAsStringSync('aaaaa');
|
||
await open(tester);
|
||
await act(tester, () => tester.tap(find.text('찾기').first));
|
||
await calm(tester);
|
||
await tester.enterText(find.widgetWithText(TextField, '이름 (비워도 됨)'), 'sub');
|
||
await tester.pump();
|
||
await act(tester, () => tester.tap(find.widgetWithText(FilledButton, '찾기')));
|
||
await settle(tester, () => find.text('찾기 결과 · 2개').evaluate().isNotEmpty && find.widgetWithText(TextButton, '모두').evaluate().isNotEmpty);
|
||
await act(tester, () => tester.tap(find.widgetWithText(TextButton, '모두')));
|
||
await calm(tester);
|
||
// 79-b: 고른 폴더 안의 파일은 세지 않고, 폴더 크기는 빠졌다고 적는다
|
||
expect(find.text('폴더 1개 (크기 미포함)'), findsOneWidget);
|
||
}
|
||
|
||
testWidgets('복사: 한 번만 - 다른 창에 sub/sub_a.txt 하나', (tester) async {
|
||
await findSubAll(tester);
|
||
await act(tester, () => tester.tap(find.text('복사 → right')));
|
||
await calm(tester);
|
||
expect(find.textContaining('sub_a.txt'), findsNothing, reason: '확인 창에도 안쪽 파일은 따로 나오지 않는다');
|
||
await act(tester, () => tester.tap(find.widgetWithText(FilledButton, '복사')));
|
||
await settle(tester, () => File(p.join(right, 'sub', 'sub_a.txt')).existsSync() && find.byType(LinearProgressIndicator).evaluate().isEmpty);
|
||
expect(File(p.join(right, 'sub_a.txt')).existsSync(), isFalse, reason: '안쪽 파일이 따로 한 번 더 복사되지 않음');
|
||
expect(find.textContaining('오류'), findsNothing);
|
||
});
|
||
|
||
testWidgets('이동: "없음" 오류 없이 한 번만 · [찾기 결과로 돌아가기] 는 새 자리', (tester) async {
|
||
await findSubAll(tester);
|
||
await act(tester, () => tester.tap(find.text('이동 → right')));
|
||
await calm(tester);
|
||
await act(tester, () => tester.tap(find.widgetWithText(FilledButton, '이동')));
|
||
await settle(tester, () => File(p.join(right, 'sub', 'sub_a.txt')).existsSync() && !Directory(p.join(left, 'sub')).existsSync() &&
|
||
find.byType(LinearProgressIndicator).evaluate().isEmpty);
|
||
expect(File(p.join(right, 'sub_a.txt')).existsSync(), isFalse);
|
||
expect(find.textContaining('없'), findsNothing, reason: '안쪽 파일을 따로 옮기려다 "없음" 이 나지 않는다');
|
||
// 79-a: 다시 찾지 않고 결과로 - 옮긴 것은 새 자리
|
||
expect(find.text('찾기 결과로 돌아가기'), findsOneWidget);
|
||
await act(tester, () => tester.tap(find.text('찾기 결과로 돌아가기')));
|
||
await settle(tester, () => find.text('찾기 결과 · 2개').evaluate().isNotEmpty);
|
||
final results = find.byType(SearchResultsPage);
|
||
expect(find.descendant(of: results, matching: find.textContaining(right)), findsNWidgets(2));
|
||
});
|
||
|
||
testWidgets('지우기: 확인 창은 1개 항목 · 폴더 1개', (tester) async {
|
||
await findSubAll(tester);
|
||
await act(tester, () => tester.tap(find.text('지우기')));
|
||
await calm(tester);
|
||
expect(find.textContaining('"sub" 을(를) 휴지통으로 보낼까요?'), findsOneWidget, reason: '안쪽 파일을 빼고 하나');
|
||
expect(find.text('그중 폴더 1개 - 안의 파일 · 폴더도 모두 함께'), findsOneWidget);
|
||
await tester.tap(find.text('휴지통을 거치지 않고 영구 삭제 (Shift+Delete)'));
|
||
await calm(tester);
|
||
await act(tester, () => tester.tap(find.widgetWithText(FilledButton, '영구 삭제')));
|
||
await settle(tester, () => !Directory(p.join(left, 'sub')).existsSync() && find.byType(LinearProgressIndicator).evaluate().isEmpty);
|
||
expect(find.textContaining('지우지 못'), findsNothing);
|
||
// 79-a: 지운 것은 결과에서 빠진다
|
||
await act(tester, () => tester.tap(find.text('찾기 결과로 돌아가기')));
|
||
await settle(tester, () => find.text('찾기 결과 · 0개').evaluate().isNotEmpty);
|
||
expect(find.text('찾은 것이 없습니다.'), findsOneWidget);
|
||
});
|
||
});
|
||
|
||
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: 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);
|
||
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: Space 로 빠른 보기 (글 파일 앞부분) · 필터에 치는 중에는 그냥 빈칸 · TC 는 F3', (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(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: '틀린 값은 비움');
|
||
});
|
||
|
||
testWidgets('80: 크기순 정렬이면 폴더 크기를 계산해 보이고, "안 함" 이어도 길게 누르기 [폴더 크기 계산]', (tester) async {
|
||
File(p.join(left, 'sub', 'big.bin')).writeAsBytesSync(List.filled(3000, 1));
|
||
c.settings
|
||
..explorerSort = 'size'
|
||
..explorerSortDesc = true;
|
||
await open(tester);
|
||
// sub = inner.txt (5) + big.bin (3000)
|
||
await settle(tester, () => find.text('3KB').evaluate().isNotEmpty);
|
||
expect(find.text('3KB'), findsWidgets);
|
||
|
||
// 바뀌면 (새로 고침) 다시 계산
|
||
File(p.join(left, 'sub', 'more.bin')).writeAsBytesSync(List.filled(3000, 1));
|
||
await act(tester, () => tester.sendKeyEvent(LogicalKeyboardKey.f5));
|
||
await settle(tester, () => find.text('6KB').evaluate().isNotEmpty);
|
||
expect(find.text('6KB'), findsWidgets);
|
||
});
|
||
|
||
testWidgets('80: "계산 안 함" - 크기를 보이지 않다가 [폴더 크기 계산] 을 누르면', (tester) async {
|
||
File(p.join(left, 'sub', 'big.bin')).writeAsBytesSync(List.filled(3000, 1));
|
||
c.settings
|
||
..explorerSort = 'size'
|
||
..explorerFolderSize = 'off';
|
||
await open(tester);
|
||
await settle(tester, () => false, rounds: 15);
|
||
expect(find.text('3KB'), findsNothing);
|
||
await tester.longPress(find.text('sub').first);
|
||
await calm(tester);
|
||
await act(tester, () => tester.tap(find.text('폴더 크기 계산')));
|
||
await settle(tester, () => find.text('3KB').evaluate().isNotEmpty);
|
||
expect(find.text('3KB'), findsWidgets);
|
||
});
|
||
|
||
testWidgets('79-a: [그 자리로] 로 나가도 [찾기 결과로 돌아가기] 가 남는다 (관리자 판단) · ✕ 로 닫음', (tester) async {
|
||
await open(tester);
|
||
await act(tester, () => tester.tap(find.text('찾기').first));
|
||
await calm(tester);
|
||
await tester.enterText(find.widgetWithText(TextField, '이름 (비워도 됨)'), 'inner');
|
||
await tester.pump();
|
||
await act(tester, () => tester.tap(find.widgetWithText(FilledButton, '찾기')));
|
||
await settle(tester, () => find.text('찾기 결과 · 1개').evaluate().isNotEmpty);
|
||
await act(tester, () => tester.tap(find.byTooltip('그 자리로')));
|
||
await settle(tester, () => find.text('찾기 결과로 돌아가기').evaluate().isNotEmpty);
|
||
await calm(tester); // 결과 화면이 닫히는 움직임까지
|
||
expect(find.byType(SearchResultsPage), findsNothing);
|
||
await act(tester, () => tester.tap(find.text('찾기 결과로 돌아가기')));
|
||
await settle(tester, () => find.text('찾기 결과 · 1개').evaluate().isNotEmpty);
|
||
// 결과 화면을 그냥 닫으면 없어진다
|
||
await act(tester, () => tester.pageBack());
|
||
await calm(tester);
|
||
expect(find.text('찾기 결과로 돌아가기'), findsNothing);
|
||
});
|
||
|
||
testWidgets('79: 찾기 결과를 목록으로 - 이름 없이 종류 조건으로 찾고, 골라서 그대로 다른 창으로 복사', (tester) async {
|
||
await open(tester);
|
||
File(p.join(left, 'sub', 'big.mkv'))
|
||
..createSync(recursive: true)
|
||
..writeAsBytesSync(List.filled(2048, 1));
|
||
await act(tester, () => tester.tap(find.text('찾기').first));
|
||
await calm(tester);
|
||
// 이름은 비우고 종류만 (동영상) - 이름이 비면 [찾기] 가 꺼져 있다가 조건을 넣으면 켜진다
|
||
expect(tester.widget<FilledButton>(find.widgetWithText(FilledButton, '찾기')).onPressed, isNull);
|
||
await act(tester, () => tester.tap(find.widgetWithText(ChoiceChip, '동영상')));
|
||
await calm(tester);
|
||
await act(tester, () => tester.tap(find.widgetWithText(FilledButton, '찾기')));
|
||
await settle(tester, () => find.text('big.mkv').evaluate().isNotEmpty);
|
||
expect(find.text('찾기 결과 · 1개'), findsOneWidget);
|
||
// (뒤의 탐색기 목록에는 doc.txt 가 있다 - 결과 화면 안만 본다)
|
||
expect(find.descendant(of: find.byType(SearchResultsPage), matching: find.text('doc.txt')), findsNothing, reason: '종류가 맞지 않음');
|
||
// 고르면 아래에 할 일 줄 - 다른 창 (right) 으로 복사
|
||
await act(tester, () => tester.tap(find.descendant(of: find.byType(SearchResultsPage), matching: find.byType(Checkbox)).first));
|
||
await calm(tester);
|
||
await act(tester, () => tester.tap(find.text('복사 → right')));
|
||
await calm(tester);
|
||
// 탐색기의 복사 확인 창 그대로
|
||
await act(tester, () => tester.tap(find.widgetWithText(FilledButton, '복사')));
|
||
await settle(tester, () => File(p.join(right, 'big.mkv')).existsSync() && find.byType(LinearProgressIndicator).evaluate().isEmpty);
|
||
expect(File(p.join(right, 'big.mkv')).lengthSync(), 2048);
|
||
expect(File(p.join(left, 'sub', 'big.mkv')).existsSync(), isTrue);
|
||
});
|
||
}
|
||
|
||
/// [name] 줄의 "표시" 동그라미 버튼
|
||
Finder markOf(String name) => find.descendant(
|
||
of: find.ancestor(of: find.text(name), matching: find.byType(Material)).first,
|
||
matching: find.byTooltip('표시 (여러 개 고르기)'),
|
||
);
|
||
|
||
/// 누르기 등을 실제 비동기 구역에서 (그 때 시작한 파일 작업이 테스트의 가짜 시간에 묶이지 않게)
|
||
Future<void> act(WidgetTester tester, Future<void> Function() f) async {
|
||
await tester.runAsync(f);
|
||
await tester.pump();
|
||
}
|
||
|
||
/// [name] 트리 줄의 › (펼치기) 화살표
|
||
Finder chevronOf(String name) => find.descendant(
|
||
of: find.ancestor(of: find.text(name), matching: find.byType(InkWell)).first,
|
||
matching: find.byIcon(Icons.chevron_right),
|
||
);
|
||
|
||
/// 두 번 누르기 (실제 비동기 구역에서: 그때 시작한 파일 작업이 테스트의 가짜 시간에 묶이지 않게)
|
||
Future<void> doubleTap(WidgetTester tester, Finder f) async {
|
||
// 흔들림: 두 누름 사이를 실제 시간 (80ms) 으로 쉬면 무거울 때 두 번 누르기 판정 (300ms) 을 넘겨 한 번씩 두 번으로 됐다 -
|
||
// 시험의 가짜 시간으로 50ms. 그 뒤 시작한 파일 작업은 settle (실제 시간) 이 기다린다
|
||
await tester.tap(f);
|
||
await tester.pump(const Duration(milliseconds: 50));
|
||
await tester.tap(f);
|
||
await tester.pump();
|
||
}
|
||
|
||
/// 창들이 목록을 다시 읽는 중 (맨 위 진행 막대 · 도는 표시) 이 끝날 때까지 실제 시간으로 기다린다.
|
||
/// 그사이 pumpAndSettle 을 하면 막대가 계속 움직여, 디스크 읽기가 늦을 때 (전체 시험이 무거울 때) 시간 초과가 난다.
|
||
/// 값이 있는 막대 (디스크 용량 · 진행률) 는 움직이지 않으므로 세지 않는다
|
||
Future<void> idle(WidgetTester tester) => settle(
|
||
tester,
|
||
() => find
|
||
.byWidgetPredicate((w) => w is ProgressIndicator && w.value == null)
|
||
.evaluate()
|
||
.isEmpty);
|
||
|
||
/// 탐색기 시험의 pumpAndSettle: 목록 읽기가 끝난 뒤에 (경로를 바꾸거나 다시 읽은 뒤 무거울 때 흔들리던 것을 한 번에)
|
||
Future<void> calm(WidgetTester tester) async {
|
||
await idle(tester);
|
||
await tester.pumpAndSettle();
|
||
}
|
||
|
||
/// 실제 파일 작업 (비동기 IO) 이 끝나기를 기다리며 화면을 갱신
|
||
// 끝나면 바로 나온다 - 넉넉한 횟수는 전체 시험 중 PC 가 바쁠 때 (실제 파일 입출력이 느려짐) 흔들리지 않게
|
||
Future<void> settle(WidgetTester tester, bool Function() done, {int rounds = 300}) async {
|
||
for (var i = 0; i < rounds; i++) {
|
||
await tester.runAsync(() => Future<void>.delayed(const Duration(milliseconds: 30)));
|
||
await tester.pump();
|
||
if (done()) break;
|
||
}
|
||
await tester.pump(const Duration(milliseconds: 300));
|
||
}
|