E5e: explorer_test (2,009) 를 6개로 나눔 · 공용 준비 explorer_harness · 시험용 이름 지움 · 약속 시험 4개
- test/support/explorer_harness.dart: 시험 폴더 · 셸 · open · act · settle · calm · doubleTap · markOf · chevronOf - explorer_panes · archive_trash · select_ops · filter_rsync · keys_drag · hidden_find (모두 600 이하) - 다른 시험 8개는 explorer_test.dart 대신 harness 를 부름 - lib: ExplorerPage.checkPathForTest · debugOpenTrash 지움 (시험은 checkEnteredPath · 실제 [휴지통] 버튼) - 약속 시험: test/explorer_more_flows_contract_test.dart (explorer_body · search_flow · tools_flow · rsync_flow) - 시험 이름 1,196 개 나누기 전후 같음, 전체 단위 1,357 (= 1,353 + 4) 2회 통과 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>split_pages
parent
79f2a2151f
commit
fad723355f
|
|
@ -21,7 +21,6 @@ import 'explorer/explorer_host.dart';
|
|||
import 'explorer/explorer_pane.dart';
|
||||
import 'explorer/file_thumbs.dart';
|
||||
import 'explorer/explorer_select.dart';
|
||||
import 'explorer/path_dialog.dart';
|
||||
import 'explorer/pane_header.dart';
|
||||
import 'explorer/pane_view.dart';
|
||||
import 'explorer/explorer_buttons.dart';
|
||||
|
|
@ -89,11 +88,6 @@ class ExplorerPage extends StatefulWidget {
|
|||
static bool get rootWarnedThisRun => ExplorerAccess.rootWarned;
|
||||
static set rootWarnedThisRun(bool v) => ExplorerAccess.rootWarned = v;
|
||||
|
||||
/// 시험: 경로 입력 창이 넣은 경로를 고쳐 쓰고 확인하는 것 (갈 경로, 칸 아래 이유)
|
||||
@visibleForTesting
|
||||
static Future<(String?, String?)> checkPathForTest(String input) =>
|
||||
checkEnteredPath(input, android: debugAndroidPaths ?? Platform.isAndroid);
|
||||
|
||||
static const routeName = 'explorer';
|
||||
static const rsyncRouteName = 'rsync';
|
||||
static int _open = 0, _openRsync = 0;
|
||||
|
|
@ -335,10 +329,6 @@ class _ExplorerPageState extends State<ExplorerPage> implements ExplorerHost, Ex
|
|||
@override
|
||||
String? get otherDir => _layout.other?.current;
|
||||
|
||||
/// 시험: [휴지통] 열기 (E5e 에서 지움)
|
||||
@visibleForTesting
|
||||
Future<void> debugOpenTrash() => _flows.deletes.openTrash(context);
|
||||
|
||||
/// 줄 메뉴 (E5d-5)
|
||||
late final _itemMenu = ExplorerItemMenu(_ctl, _sel, this, _flows, nomedia: () => ExplorerPage.debugNomedia ?? Platform.isAndroid);
|
||||
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ import 'package:jj_mkvmaker/ui/monitor_page.dart';
|
|||
import 'package:jj_mkvmaker/ui/sync_preview_view.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
import 'explorer_test.dart' show act, settle;
|
||||
import 'support/explorer_harness.dart' show act, settle;
|
||||
|
||||
/// 110: 확인 창은 뒤로 가기 · 바깥 누르기 · [취소] 로 닫으면 아무것도 하지 않는다 ([실행] · [지우기] 등만 실행)
|
||||
void main() {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,194 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:archive/archive.dart' show Archive, ArchiveFile, ZipEncoder;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:jj_mkvmaker/ui/explorer_page.dart';
|
||||
import 'package:jj_mkvmaker/ui/explorer_grid.dart';
|
||||
import 'package:jj_mkvmaker/ui/text_viewer.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'support/explorer_clock_keys.dart';
|
||||
import 'support/explorer_harness.dart';
|
||||
|
||||
/// E5e: 예전 explorer_test.dart 에서 나눈 탐색기 화면 시험 - 압축을 폴더처럼 · 격자 · 글 뷰어 · 앱 휴지통 · 원본 지우기 옵션 알림 · 바로 실행
|
||||
void main() {
|
||||
useExplorerFixture();
|
||||
|
||||
testWidgets('X-plore f: 압축을 트리 안에서 폴더처럼 펼치기 · 안은 읽기 전용 · 복사는 꺼내기', (tester) async {
|
||||
final zip = p.join(left, 'pack.zip');
|
||||
File(zip).writeAsBytesSync(ZipEncoder().encode(Archive()
|
||||
..addFile(ArchiveFile.string('top.txt', 'top'))
|
||||
..addFile(ArchiveFile.string('a/b.txt', 'bee'))));
|
||||
await open(tester);
|
||||
await settle(tester, () => find.text('pack.zip').evaluate().isNotEmpty);
|
||||
// › 로 펼치면 압축 안의 것이 폴더처럼 (목록에 폴더 이름이 없어도 경로에서 폴더를 만든다)
|
||||
await act(tester, () => tester.tap(chevronOf('pack.zip').first));
|
||||
await settle(tester, () => find.text('top.txt').evaluate().isNotEmpty);
|
||||
expect(find.text('a'), findsOneWidget);
|
||||
await act(tester, () => tester.tap(chevronOf('a').first));
|
||||
await settle(tester, () => find.text('b.txt').evaluate().isNotEmpty);
|
||||
expect(find.text('b.txt'), findsOneWidget);
|
||||
// 안의 것을 지우려 하면 막는다 (압축은 그대로)
|
||||
await act(tester, () => tester.tap(find.text('top.txt')));
|
||||
await settle(tester, () => false, rounds: 10);
|
||||
await act(tester, () => tester.tap(find.text('삭제').first));
|
||||
await settle(tester, () => find.textContaining('읽기 전용').evaluate().isNotEmpty);
|
||||
expect(find.textContaining('압축 안은 읽기 전용'), findsOneWidget);
|
||||
expect(File(zip).existsSync(), isTrue);
|
||||
expect(Directory(p.join(left, 'pack')).existsSync(), isFalse, reason: '풀지 않음');
|
||||
ScaffoldMessenger.of(tester.element(find.byType(ExplorerPage))).clearSnackBars();
|
||||
await tester.pump(const Duration(seconds: 1));
|
||||
// 압축 안의 폴더를 펼쳐 지금 폴더가 압축 안이면 [새 폴더] 도 막는다 (이름을 묻지 않음)
|
||||
await act(tester, () => tester.tap(find.text('a')));
|
||||
await settle(tester, () => false, rounds: 10);
|
||||
await act(tester, () => tester.tap(find.text('새 폴더').first));
|
||||
await settle(tester, () => find.textContaining('읽기 전용').evaluate().isNotEmpty);
|
||||
expect(find.textContaining('압축 안은 읽기 전용'), findsOneWidget);
|
||||
expect(find.byType(TextField), findsNothing, reason: '이름 묻는 창 없음');
|
||||
ScaffoldMessenger.of(tester.element(find.byType(ExplorerPage))).clearSnackBars();
|
||||
await tester.pump(const Duration(seconds: 1));
|
||||
// 데이터 검토 (10/18): 압축 안의 것 메뉴에는 [ZIP 으로 묶기] · [Wi-Fi 로 공유] 가 없다 (실제 파일이 아니라서). 압축 밖은 그대로
|
||||
await tester.longPress(find.text('a'));
|
||||
await calm(tester);
|
||||
expect(find.text('ZIP 으로 묶기'), findsNothing);
|
||||
expect(find.text('Wi-Fi 로 공유'), findsNothing);
|
||||
await tester.tapAt(const Offset(5, 5));
|
||||
await calm(tester);
|
||||
await tester.longPress(find.text('sub'));
|
||||
await calm(tester);
|
||||
expect(find.text('ZIP 으로 묶기'), findsOneWidget);
|
||||
await tester.tapAt(const Offset(5, 5));
|
||||
await calm(tester);
|
||||
});
|
||||
|
||||
testWidgets('X-plore f 보충: 펼친 뒤 압축 파일이 바뀌면 새로 고칠 때 목록을 다시 읽는다 (관리자 지시 10/19)', (tester) async {
|
||||
final zip = p.join(left, 'pack.zip');
|
||||
File(zip).writeAsBytesSync(ZipEncoder().encode(Archive()..addFile(ArchiveFile.string('old.txt', 'old'))));
|
||||
await open(tester);
|
||||
await settle(tester, () => find.text('pack.zip').evaluate().isNotEmpty);
|
||||
await act(tester, () => tester.tap(chevronOf('pack.zip').first));
|
||||
await settle(tester, () => find.text('old.txt').evaluate().isNotEmpty);
|
||||
expect(find.text('old.txt'), findsOneWidget);
|
||||
// 다른 프로그램이 압축을 바꿈 (크기 · 바뀐 때가 달라짐)
|
||||
await tester.runAsync(() async {
|
||||
await Future<void>.delayed(const Duration(milliseconds: 1100));
|
||||
File(zip).writeAsBytesSync(ZipEncoder().encode(Archive()
|
||||
..addFile(ArchiveFile.string('new.txt', 'new content'))
|
||||
..addFile(ArchiveFile.string('more.txt', 'more'))));
|
||||
});
|
||||
await act(tester, () => refreshKey(tester));
|
||||
await settle(tester, () => find.text('new.txt').evaluate().isNotEmpty);
|
||||
expect(find.text('new.txt'), findsOneWidget);
|
||||
expect(find.text('more.txt'), findsOneWidget);
|
||||
expect(find.text('old.txt'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('X-plore g · h: 폴더 메뉴 [격자로 보기] (길게 = 그 자리로) · 파일 메뉴 [글 뷰어로 보기]', (tester) async {
|
||||
File(p.join(left, 'sub', 'pic.png')).writeAsBytesSync([0x89, 0x50]);
|
||||
await open(tester);
|
||||
// g: 폴더를 길게 → 격자로 보기 → 그림만 (글 파일은 빠짐)
|
||||
await tester.longPress(find.text('sub'));
|
||||
await calm(tester);
|
||||
await act(tester, () => tester.tap(find.text('격자로 보기 (그림 · 동영상)')));
|
||||
await settle(tester, () => find.byType(ExplorerGridPage).evaluate().isNotEmpty);
|
||||
expect(find.descendant(of: find.byType(ExplorerGridPage), matching: find.text('pic.png')), findsOneWidget);
|
||||
expect(find.descendant(of: find.byType(ExplorerGridPage), matching: find.text('inner.txt')), findsNothing);
|
||||
// 칸을 길게 누르면 탐색기에서 그 자리로 (격자 닫힘)
|
||||
await tester.longPress(find.descendant(of: find.byType(ExplorerGridPage), matching: find.text('pic.png')));
|
||||
await settle(tester, () => find.byType(ExplorerGridPage).evaluate().isEmpty && find.descendant(of: find.byType(ExplorerPage), matching: find.text('pic.png')).evaluate().isNotEmpty);
|
||||
expect(find.descendant(of: find.byType(ExplorerPage), matching: find.text('pic.png')), findsOneWidget, reason: '탐색기에 그 폴더가 펼쳐져 보임 (길게 누른 칸의 이름 풍선은 빼고)');
|
||||
// h: 파일을 길게 → 글 뷰어로 보기 (읽기 전용)
|
||||
await tester.longPress(find.text('doc.txt'));
|
||||
await calm(tester);
|
||||
await act(tester, () => tester.tap(find.text('글 뷰어로 보기')));
|
||||
await settle(tester, () => find.text('hello').evaluate().isNotEmpty);
|
||||
expect(find.byType(TextViewerPage), findsOneWidget);
|
||||
expect(shell.opened, isEmpty, reason: '다른 앱으로 열지 않음');
|
||||
await act(tester, () async => Navigator.of(tester.element(find.byType(TextViewerPage))).pop());
|
||||
await settle(tester, () => find.byType(TextViewerPage).evaluate().isEmpty);
|
||||
});
|
||||
|
||||
testWidgets('X-plore e: Android 앱 휴지통 - 지우기 = 휴지통으로 · [되돌리기] · [휴지통] 화면에서 되살리기', (tester) async {
|
||||
// 저장 장치는 시험 폴더 (실제 C:\ 에 .jj_trash 를 만들지 않게)
|
||||
ExplorerPage.debugAndroidTrash = true;
|
||||
ExplorerPage.debugVolumes = () => [(tmp.path, 'T:')];
|
||||
// [휴지통] 버튼이 줄에 보이게 (처음 구성은 좁으면 [더 보기] 로 간다)
|
||||
c.settings.explorerButtons = ['trash', for (final b in ExplorerButton.defaultOrder) if (b != ExplorerButton.trash) b.name];
|
||||
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);
|
||||
// [휴지통] 버튼으로 휴지통 화면을 연다
|
||||
await act(tester, () => tester.tap(find.text('휴지통').first));
|
||||
await settle(tester, () => find.byType(CheckboxListTile).evaluate().isNotEmpty);
|
||||
await act(tester, () => tester.tap(find.byType(CheckboxListTile).first));
|
||||
await settle(tester, () => find.text('되살리기').hitTestable().evaluate().isNotEmpty); // 화면 넘김이 끝나 눌릴 때까지
|
||||
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));
|
||||
});
|
||||
}
|
||||
|
|
@ -7,6 +7,7 @@ import 'package:jj_mkvmaker/app/app_controller.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/platform_services.dart';
|
||||
import 'package:jj_mkvmaker/ui/explorer/path_dialog.dart' show checkEnteredPath;
|
||||
import 'package:jj_mkvmaker/ui/explorer_page.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
|
|
@ -14,7 +15,7 @@ import 'support/fake_recycle_bin.dart';
|
|||
import 'support/recycle_leftovers.dart';
|
||||
import 'support/elevated.dart';
|
||||
|
||||
import 'explorer_test.dart' show calm;
|
||||
import 'support/explorer_harness.dart' show calm;
|
||||
/// 48 · 154 · 155: 같은 이름이 있으면 덮어쓰기 · 건너뛰기 · 이름 바꾸기를 고르고, 확인 창에 항목 이름이 보인다
|
||||
void main() {
|
||||
late Directory tmp;
|
||||
|
|
@ -448,7 +449,7 @@ void main() {
|
|||
expect(smbToUnc(r'\\nas\share'), isNull);
|
||||
expect(smbToUnc('D:/x'), isNull);
|
||||
if (!Platform.isWindows) return;
|
||||
final (path, error) = await ExplorerPage.checkPathForTest('smb://localhost/jj_no_such_share_190');
|
||||
final (path, error) = await checkEnteredPath('smb://localhost/jj_no_such_share_190', android: false);
|
||||
expect(path, isNull);
|
||||
expect(error, contains(r'\\localhost\jj_no_such_share_190'));
|
||||
expect(error, contains('형식'));
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import 'package:jj_mkvmaker/services/platform_services.dart';
|
|||
import 'package:jj_mkvmaker/ui/explorer_page.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
import 'explorer_test.dart' show calm;
|
||||
import 'support/explorer_harness.dart' show calm;
|
||||
void main() {
|
||||
late Directory tmp;
|
||||
late AppController c;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,389 @@
|
|||
import 'dart:io';
|
||||
|
||||
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/explorer_search.dart';
|
||||
import 'package:jj_mkvmaker/ui/path_label.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:jj_mkvmaker/ui/sync_preview_view.dart' show keepOptionsTogether;
|
||||
import 'support/explorer_harness.dart';
|
||||
|
||||
/// E5e: 예전 explorer_test.dart 에서 나눈 탐색기 화면 시험 - 빠른 필터 · 여러 개 지우기 · 새 폴더 · 이름 변경 · 창 배치 · 버튼 구성 · Rsync 화면 · 일괄 이름 · 폴더 + 안의 파일
|
||||
void main() {
|
||||
useExplorerFixture();
|
||||
|
||||
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(keepOptionsTogether('원본 파일 지우기 (--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);
|
||||
// 옵션 낱말 -u 만 (폴더 이름 vscode-stable-user-x64 같은 것은 아님 - 시험 임시 폴더 위가 %TEMP% 일 때)
|
||||
expect(find.textContaining(RegExp(r'(^|\s)-u(\s|$)')), findsNothing);
|
||||
expect(find.text('빈 폴더 지움 · 원본 폴더는 남김'), findsNothing);
|
||||
await tester.tap(find.text(keepOptionsTogether('원본 파일 지우기 (--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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,304 @@
|
|||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.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/hidden_items_dialog.dart';
|
||||
import 'package:jj_mkvmaker/ui/explorer_search.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'support/explorer_clock_keys.dart';
|
||||
import 'support/explorer_harness.dart';
|
||||
|
||||
/// E5e: 예전 explorer_test.dart 에서 나눈 탐색기 화면 시험 - 목록에서 숨기기 · .nomedia · 폴더 크기 · 찾기 결과
|
||||
void main() {
|
||||
useExplorerFixture();
|
||||
|
||||
testWidgets('X-plore 숨기기: 길게 누르기 [목록에서 숨기기] → 목록에서만 빠짐 (파일 그대로) · 맨 아래 [숨긴 항목 보이기] · [숨기기 풀기]', (tester) async {
|
||||
// 폴더 + 파일 목록 배치: 오른쪽 목록은 그 폴더의 것만이라 짧다 (트리는 Temp 폴더 줄이 많아 맨 아래가 멀다)
|
||||
c.settings.explorerLayout = 'split';
|
||||
await open(tester);
|
||||
final doc = p.join(left, 'doc.txt');
|
||||
await tester.longPress(find.text('doc.txt'));
|
||||
await calm(tester);
|
||||
await act(tester, () => tester.tap(find.text('목록에서 숨기기')));
|
||||
await calm(tester);
|
||||
// X-plore 맞추기 (관리자 지시 10/20): 무엇이 바뀌는지 확인 창
|
||||
expect(find.textContaining('파일 탐색기 목록에서만 빠집니다'), findsOneWidget);
|
||||
await act(tester, () => tester.tap(find.widgetWithText(FilledButton, '숨기기')));
|
||||
await settle(tester, () => find.text('doc.txt').evaluate().isEmpty);
|
||||
expect(find.text('doc.txt'), findsNothing);
|
||||
expect(File(doc).existsSync(), isTrue, reason: '파일은 바꾸지 않는다');
|
||||
expect(c.settings.explorerHiddenItems, [doc]);
|
||||
// 목록 맨 아래에 [숨긴 항목 1개 보이기]
|
||||
expect(find.text('숨긴 항목 1개 보이기'), findsOneWidget);
|
||||
await idle(tester);
|
||||
// 보이기 → 흐리게 보이고, 메뉴는 [숨기기 풀기]
|
||||
await act(tester, () => tester.tap(find.text('숨긴 항목 1개 보이기')));
|
||||
await settle(tester, () => find.text('doc.txt').evaluate().isNotEmpty);
|
||||
expect(find.text('숨긴 항목 1개 다시 숨기기'), findsOneWidget);
|
||||
expect(find.ancestor(of: find.text('doc.txt'), matching: find.byType(Opacity)), findsOneWidget);
|
||||
await tester.longPress(find.text('doc.txt'));
|
||||
await calm(tester);
|
||||
expect(find.text('목록에서 숨기기'), findsNothing);
|
||||
await act(tester, () => tester.tap(find.text('숨기기 풀기')));
|
||||
await settle(tester, () => find.textContaining('숨긴 항목').evaluate().isEmpty);
|
||||
expect(c.settings.explorerHiddenItems, isEmpty);
|
||||
expect(find.text('doc.txt'), findsOneWidget);
|
||||
expect(find.ancestor(of: find.text('doc.txt'), matching: find.byType(Opacity)), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('X-plore 숨기기 확인 창: [취소] 면 그대로 · [다음부터 묻지 않기] 를 고르면 다음엔 묻지 않고 바로 숨김 (설정 저장)', (tester) async {
|
||||
c.settings.explorerLayout = 'split';
|
||||
expect(AppSettings().explorerHideConfirm, isTrue, reason: '처음은 묻기');
|
||||
File(p.join(left, 'two.txt')).writeAsStringSync('2');
|
||||
await open(tester);
|
||||
await tester.longPress(find.text('doc.txt'));
|
||||
await calm(tester);
|
||||
await act(tester, () => tester.tap(find.text('목록에서 숨기기')));
|
||||
await calm(tester);
|
||||
await act(tester, () => tester.tap(find.text('취소')));
|
||||
await calm(tester);
|
||||
expect(c.settings.explorerHiddenItems, isEmpty, reason: '취소하면 숨기지 않는다');
|
||||
expect(find.text('doc.txt'), findsOneWidget);
|
||||
// 다음부터 묻지 않기 + 숨기기
|
||||
await tester.longPress(find.text('doc.txt'));
|
||||
await calm(tester);
|
||||
await act(tester, () => tester.tap(find.text('목록에서 숨기기')));
|
||||
await calm(tester);
|
||||
await act(tester, () => tester.tap(find.text('다음부터 묻지 않기')));
|
||||
await act(tester, () => tester.tap(find.widgetWithText(FilledButton, '숨기기')));
|
||||
await settle(tester, () => find.text('doc.txt').evaluate().isEmpty);
|
||||
expect(c.settings.explorerHideConfirm, isFalse);
|
||||
expect(AppSettings.fromJson(c.settings.toJson()).explorerHideConfirm, isFalse);
|
||||
expect(AppSettings.fromJson({}).explorerHideConfirm, isTrue, reason: '예전 설정 (값 없음) 은 묻기');
|
||||
// 이제는 창 없이 바로
|
||||
await tester.longPress(find.text('two.txt'));
|
||||
await calm(tester);
|
||||
await act(tester, () => tester.tap(find.text('목록에서 숨기기')));
|
||||
await settle(tester, () => find.text('two.txt').evaluate().isEmpty);
|
||||
expect(find.textContaining('파일 탐색기 목록에서만 빠집니다'), findsNothing);
|
||||
expect(c.settings.explorerHiddenItems, [p.join(left, 'doc.txt'), p.join(left, 'two.txt')]);
|
||||
});
|
||||
|
||||
testWidgets('X-plore 맞추기: 폴더 길게 누르기 [갤러리 · 음악 앱에서 숨기기 (.nomedia)] - 확인 창 뒤 빈 .nomedia, 다시 누르면 지우기 · 안의 파일은 그대로', (tester) async {
|
||||
ExplorerPage.debugNomedia = true;
|
||||
addTearDown(() => ExplorerPage.debugNomedia = null);
|
||||
final mark = File(p.join(left, 'sub', '.nomedia'));
|
||||
await open(tester);
|
||||
expect(find.text('갤러리 · 음악 앱에서 숨기기 (.nomedia)'), findsNothing);
|
||||
await tester.longPress(find.text('sub'));
|
||||
await calm(tester);
|
||||
await act(tester, () => tester.tap(find.text('갤러리 · 음악 앱에서 숨기기 (.nomedia)')));
|
||||
await calm(tester);
|
||||
expect(find.textContaining('빈 .nomedia 파일을 만듭니다'), findsOneWidget);
|
||||
// 취소하면 아무것도 안 바뀜
|
||||
await act(tester, () => tester.tap(find.text('취소')));
|
||||
await calm(tester);
|
||||
expect(mark.existsSync(), isFalse);
|
||||
await tester.longPress(find.text('sub'));
|
||||
await calm(tester);
|
||||
await act(tester, () => tester.tap(find.text('갤러리 · 음악 앱에서 숨기기 (.nomedia)')));
|
||||
await calm(tester);
|
||||
await act(tester, () => tester.tap(find.widgetWithText(FilledButton, '숨기기')));
|
||||
await settle(tester, () => mark.existsSync() && shell.scanned.isNotEmpty);
|
||||
expect(mark.lengthSync(), 0);
|
||||
expect(shell.scanned, [
|
||||
[mark.path, p.join(left, 'sub')]
|
||||
], reason: '갤러리가 다시 읽게 (MediaScanner)');
|
||||
expect(File(p.join(left, 'sub', 'inner.txt')).readAsStringSync(), 'inner', reason: '안의 파일은 그대로');
|
||||
// 이제 메뉴는 [다시 보이기]
|
||||
await idle(tester);
|
||||
await tester.longPress(find.text('sub'));
|
||||
await calm(tester);
|
||||
expect(find.text('갤러리 · 음악 앱에서 숨기기 (.nomedia)'), findsNothing);
|
||||
await act(tester, () => tester.tap(find.text('갤러리 · 음악 앱에 다시 보이기 (.nomedia 지우기)')));
|
||||
await calm(tester);
|
||||
expect(find.textContaining('.nomedia 파일을 지웁니다'), findsOneWidget);
|
||||
await act(tester, () => tester.tap(find.widgetWithText(FilledButton, '다시 보이기')));
|
||||
await settle(tester, () => !mark.existsSync());
|
||||
await settle(tester, () => shell.scanned.length == 2);
|
||||
expect(shell.scanned.last, [mark.path, p.join(left, 'sub')], reason: '지울 때도 다시 읽게');
|
||||
expect(File(p.join(left, 'sub', 'inner.txt')).existsSync(), isTrue);
|
||||
// 파일에는 없음
|
||||
await idle(tester);
|
||||
await tester.longPress(find.text('doc.txt'));
|
||||
await calm(tester);
|
||||
expect(find.text('갤러리 · 음악 앱에서 숨기기 (.nomedia)'), findsNothing);
|
||||
expect(find.text('갤러리 · 음악 앱에 다시 보이기 (.nomedia 지우기)'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('X-plore 숨기기: 숨긴 것이 든 폴더를 지울 때 확인 창에 알림 (숨긴 것도 함께 지워진다)', (tester) async {
|
||||
c.settings.explorerHiddenItems = [p.join(left, 'sub', 'inner.txt')];
|
||||
await open(tester);
|
||||
await tester.longPress(find.text('sub'));
|
||||
await calm(tester);
|
||||
await act(tester, () => tester.tap(find.byTooltip('삭제').last));
|
||||
await calm(tester);
|
||||
expect(find.text('지우는 폴더 안에 목록에서 숨긴 항목 1개가 있습니다 - 함께 지워집니다'), findsOneWidget);
|
||||
await act(tester, () => tester.tap(find.text('취소').last));
|
||||
await calm(tester);
|
||||
expect(File(p.join(left, 'sub', 'inner.txt')).existsSync(), isTrue);
|
||||
// 다른 파일만 지울 때는 알리지 않는다
|
||||
await tester.longPress(find.text('doc.txt'));
|
||||
await calm(tester);
|
||||
await act(tester, () => tester.tap(find.byTooltip('삭제').last));
|
||||
await calm(tester);
|
||||
expect(find.textContaining('목록에서 숨긴 항목'), findsNothing);
|
||||
await act(tester, () => tester.tap(find.text('취소').last));
|
||||
await calm(tester);
|
||||
});
|
||||
|
||||
testWidgets('X-plore 숨기기: 이름을 바꾸면 숨긴 목록도 새 경로로 · 영구로 지우면 목록에서 뺀다 (관리자 결정)', (tester) async {
|
||||
c.settings.explorerHiddenItems = [p.join(left, 'sub', 'inner.txt')];
|
||||
await open(tester);
|
||||
await tester.longPress(find.text('sub'));
|
||||
await calm(tester);
|
||||
await act(tester, () => tester.tap(find.byTooltip('이름 변경').last));
|
||||
await calm(tester);
|
||||
await act(tester, () => tester.enterText(find.byType(TextField), 'sub2'));
|
||||
await act(tester, () => tester.tap(find.widgetWithText(FilledButton, '확인')));
|
||||
await settle(tester, () => find.text('sub2').evaluate().isNotEmpty);
|
||||
expect(c.settings.explorerHiddenItems, [p.join(left, 'sub2', 'inner.txt')]);
|
||||
await idle(tester);
|
||||
await tester.longPress(find.text('sub2'));
|
||||
await calm(tester);
|
||||
await act(tester, () => tester.tap(find.byTooltip('삭제').last));
|
||||
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.text('sub2').evaluate().isEmpty);
|
||||
await settle(tester, () => c.settings.explorerHiddenItems.isEmpty);
|
||||
expect(c.settings.explorerHiddenItems, isEmpty);
|
||||
});
|
||||
|
||||
testWidgets('X-plore 숨기기: 다른 창으로 옮기면 숨긴 목록도 새 자리로 (그 안의 것도)', (tester) async {
|
||||
c.settings.explorerHiddenItems = [p.join(left, 'sub', 'inner.txt')];
|
||||
await open(tester);
|
||||
await tester.longPress(find.text('sub'));
|
||||
await calm(tester);
|
||||
await act(tester, () => tester.tap(find.byTooltip('다른 창으로 이동').last));
|
||||
await settle(tester, () => find.widgetWithText(FilledButton, '이동').evaluate().isNotEmpty);
|
||||
await act(tester, () => tester.tap(find.widgetWithText(FilledButton, '이동')));
|
||||
await settle(tester, () => c.settings.explorerHiddenItems.first != p.join(left, 'sub', 'inner.txt'));
|
||||
expect(File(p.join(right, 'sub', 'inner.txt')).existsSync(), isTrue);
|
||||
expect(c.settings.explorerHiddenItems, [p.join(right, 'sub', 'inner.txt')]);
|
||||
await idle(tester);
|
||||
});
|
||||
|
||||
testWidgets('X-plore 숨기기: 위 막대 ⋮ [목록에서 숨긴 항목 보이기] · 환경 설정의 [목록에서 숨긴 항목] 에서 풀기', (tester) async {
|
||||
final doc = p.join(left, 'doc.txt');
|
||||
c.settings
|
||||
..explorerLayout = 'split'
|
||||
..explorerHiddenItems = [doc];
|
||||
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('숨긴 항목 1개 보이기').evaluate().isNotEmpty);
|
||||
expect(find.text('doc.txt'), findsNothing);
|
||||
await act(tester, () => tester.tap(find.byTooltip('창 배치 · 버튼 구성')));
|
||||
await calm(tester);
|
||||
await act(tester, () => tester.tap(find.text('목록에서 숨긴 항목 보이기 (1개)')));
|
||||
await settle(tester, () => find.text('doc.txt').evaluate().isNotEmpty);
|
||||
expect(find.text('숨긴 항목 1개 다시 숨기기'), findsOneWidget);
|
||||
await act(tester, () => tester.tap(find.byTooltip('창 배치 · 버튼 구성')));
|
||||
await calm(tester);
|
||||
await act(tester, () => tester.tap(find.text('목록에서 숨긴 항목 다시 숨기기')));
|
||||
await settle(tester, () => find.text('doc.txt').evaluate().isEmpty);
|
||||
// 설정 화면의 목록에서 풀면 탐색기 목록에 바로 보인다
|
||||
final ctx = tester.element(find.byType(ExplorerPage));
|
||||
unawaited(showHiddenItems(ctx, c));
|
||||
await calm(tester);
|
||||
expect(find.text('목록에서 숨긴 항목 (1개)'), findsOneWidget);
|
||||
await act(tester, () => tester.tap(find.byTooltip('숨기기 풀기')));
|
||||
await calm(tester);
|
||||
expect(c.settings.explorerHiddenItems, isEmpty);
|
||||
expect(find.text('숨긴 항목이 없습니다'), findsOneWidget);
|
||||
await act(tester, () => tester.tap(find.text('닫기')));
|
||||
await settle(tester, () => find.text('doc.txt').evaluate().isNotEmpty);
|
||||
expect(find.textContaining('숨긴 항목'), findsNothing);
|
||||
});
|
||||
|
||||
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, () => refreshKey(tester));
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,379 @@
|
|||
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: '틀린 값은 비움');
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,225 @@
|
|||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:jj_mkvmaker/app/app_controller.dart';
|
||||
import 'package:jj_mkvmaker/core/file_ops.dart' show FileEntry;
|
||||
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/platform_services.dart';
|
||||
import 'package:jj_mkvmaker/ui/explorer/explorer_bars.dart';
|
||||
import 'package:jj_mkvmaker/ui/explorer/explorer_body.dart';
|
||||
import 'package:jj_mkvmaker/ui/explorer/explorer_buttons.dart';
|
||||
import 'package:jj_mkvmaker/ui/explorer/explorer_controller.dart';
|
||||
import 'package:jj_mkvmaker/ui/explorer/explorer_flows.dart';
|
||||
import 'package:jj_mkvmaker/ui/explorer/explorer_host.dart';
|
||||
import 'package:jj_mkvmaker/ui/explorer/explorer_layout.dart';
|
||||
import 'package:jj_mkvmaker/ui/explorer/explorer_pane.dart';
|
||||
import 'package:jj_mkvmaker/ui/explorer/explorer_select.dart';
|
||||
import 'package:jj_mkvmaker/ui/explorer/pane_header.dart';
|
||||
import 'package:jj_mkvmaker/ui/explorer/pane_view.dart';
|
||||
import 'package:jj_mkvmaker/ui/explorer/server_flows.dart';
|
||||
import 'package:jj_mkvmaker/ui/explorer/toolbar.dart';
|
||||
import 'package:jj_mkvmaker/ui/explorer_drag.dart' show DragLoad;
|
||||
import 'package:jj_mkvmaker/ui/explorer_search.dart' show SearchResultsPage;
|
||||
import 'package:jj_mkvmaker/ui/storage_page.dart' show StorageAnalysisPage;
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
/// E5e 약속 시험: explorer_body · search_flow · tools_flow · rsync_flow 맨 위 약속
|
||||
/// (실제 시험 폴더 · 실제 화면 · 실제 설정 (저장하지 않음). rsync 실행은 rsync 실행 파일이 있어야 해 확인 창까지)
|
||||
void main() {
|
||||
late Directory tmp;
|
||||
late String root, l, r;
|
||||
setUp(() {
|
||||
tmp = Directory.systemTemp.createTempSync('jj_more_flow_');
|
||||
root = tmp.path;
|
||||
l = p.join(root, 'L');
|
||||
r = p.join(root, 'R');
|
||||
File(p.join(l, 'doc.txt'))
|
||||
..createSync(recursive: true)
|
||||
..writeAsStringSync('hello');
|
||||
Directory(r).createSync();
|
||||
});
|
||||
tearDown(() => tmp.deleteSync(recursive: true));
|
||||
|
||||
Future<void> settle(WidgetTester t, [bool Function()? until]) async {
|
||||
for (var i = 0; i < 40 && !(until?.call() ?? false); i++) {
|
||||
await t.runAsync(() => Future<void>.delayed(const Duration(milliseconds: 30)));
|
||||
await t.pump(const Duration(milliseconds: 100));
|
||||
}
|
||||
}
|
||||
|
||||
Future<_HostState> open(WidgetTester t, {bool rsync = false, Size size = const Size(1200, 800)}) async {
|
||||
await t.binding.setSurfaceSize(size);
|
||||
addTearDown(() => t.binding.setSurfaceSize(null));
|
||||
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||
final key = GlobalKey<_HostState>();
|
||||
await t.pumpWidget(MaterialApp(home: _Host(key: key)));
|
||||
final host = key.currentState!;
|
||||
host.make(c, rsync: rsync, root: root);
|
||||
addTearDown(host.drop);
|
||||
await t.runAsync(() => host.ctl.goTo(host.ctl.panes[0], l));
|
||||
await t.runAsync(() => host.ctl.goTo(host.ctl.panes[1], r));
|
||||
host.ctl.ready = true;
|
||||
host.redraw();
|
||||
await t.pump();
|
||||
return host;
|
||||
}
|
||||
|
||||
testWidgets('창 배치: 두 창 = 창 둘 + 버튼 줄 · 한 창 = 창 하나 · 폴더 + 파일 목록 = 트리 + 목록 · 좁은 Rsync = [왼쪽 | 오른쪽]', (t) async {
|
||||
final h = await open(t);
|
||||
expect([find.byType(ExplorerPaneView).evaluate().length, find.byType(ExplorerToolbar).evaluate().length], [2, 1]);
|
||||
h.ctl.c.settings.explorerLayout = 'single';
|
||||
h.redraw();
|
||||
await t.pump();
|
||||
expect(find.byType(ExplorerPaneView).evaluate().length, 1);
|
||||
h.ctl.c.settings.explorerLayout = 'split';
|
||||
h.redraw();
|
||||
await t.pump();
|
||||
expect([find.byType(ExplorerPaneView).evaluate().length, find.byType(ExplorerListView).evaluate().length], [1, 1]);
|
||||
h.ctl.c.settings.explorerToolbar = 'hidden';
|
||||
h.redraw();
|
||||
await t.pump();
|
||||
expect(find.byType(ExplorerToolbar), findsNothing, reason: '버튼 줄 숨김');
|
||||
final rs = await open(t, rsync: true, size: const Size(420, 800));
|
||||
expect([find.byType(RsyncPaneSwitch).evaluate().length, find.byType(ExplorerPaneView).evaluate().length, rs.ctl.rsync], [1, 1, true]);
|
||||
});
|
||||
|
||||
testWidgets('찾기: 처음엔 돌아갈 결과 없음 · 조건 창 [찾기] → 결과 화면에 찾은 것 · 그냥 닫으면 돌아갈 결과도 없음', (t) async {
|
||||
final h = await open(t);
|
||||
expect(h.flows.search.backBar(h.context), isNull);
|
||||
unawaited(h.flows.search.open(h.context, h.ctl.panes[0]));
|
||||
await t.pump();
|
||||
await t.enterText(find.byType(TextField).first, 'doc');
|
||||
await t.pump();
|
||||
await t.tap(find.widgetWithText(FilledButton, '찾기'));
|
||||
await settle(t, () => find.byType(SearchResultsPage).evaluate().isNotEmpty && find.text('doc.txt').evaluate().length > 1);
|
||||
expect(find.byType(SearchResultsPage), findsOneWidget);
|
||||
Navigator.of(t.element(find.byType(SearchResultsPage))).pop();
|
||||
await settle(t);
|
||||
expect(h.flows.search.backBar(h.context), isNull);
|
||||
});
|
||||
|
||||
testWidgets('저장 공간: 원격은 먼저 묻고 [취소] 면 그대로 · 이 기기 폴더는 분석 화면 (고르지 않고 닫으면 그대로)', (t) async {
|
||||
final h = await open(t);
|
||||
final q = h.ctl.panes[0];
|
||||
unawaited(h.flows.tools.openStorage(h.context, q, 'dav://srv/x/'));
|
||||
await t.pump();
|
||||
expect(find.textContaining('오래 걸릴 수 있습니다'), findsOneWidget);
|
||||
await t.tap(find.text('취소'));
|
||||
await t.pump();
|
||||
expect(find.byType(StorageAnalysisPage), findsNothing);
|
||||
var done = false;
|
||||
unawaited(h.flows.tools.openStorage(h.context, q, l).then((_) => done = true));
|
||||
await settle(t, () => find.byType(StorageAnalysisPage).evaluate().isNotEmpty);
|
||||
expect(find.byType(StorageAnalysisPage), findsOneWidget);
|
||||
Navigator.of(t.element(find.byType(StorageAnalysisPage))).pop();
|
||||
await settle(t, () => done);
|
||||
expect([done, q.current], [true, l]);
|
||||
});
|
||||
|
||||
testWidgets('Rsync: 고르지 않으면 까닭 알림 · 두 폴더를 고르면 확인 창 → [취소] 면 실행 안 함', (t) async {
|
||||
final h = await open(t, rsync: true);
|
||||
await t.runAsync(() => h.flows.rsync.run(h.context, right: true, left: false));
|
||||
await t.pump();
|
||||
expect(find.text('왼쪽 · 오른쪽 창에서 폴더를 하나씩 고르세요.'), findsOneWidget);
|
||||
await t.runAsync(() => h.sel.pick(h.ctl.panes[0], FileEntry(l, isDir: true, modified: DateTime(0))));
|
||||
await t.runAsync(() => h.sel.pick(h.ctl.panes[1], FileEntry(r, isDir: true, modified: DateTime(0))));
|
||||
var done = false;
|
||||
unawaited(h.flows.rsync.run(h.context, right: true, left: false).then((_) => done = true));
|
||||
await settle(t, () => find.text('취소').evaluate().isNotEmpty);
|
||||
await t.tap(find.text('취소').last);
|
||||
await settle(t, () => done);
|
||||
expect([done, File(p.join(r, 'doc.txt')).existsSync(), h.flows.jobs.list.isEmpty], [true, false, true]);
|
||||
});
|
||||
}
|
||||
|
||||
class _Host extends StatefulWidget {
|
||||
const _Host({super.key});
|
||||
@override
|
||||
State<_Host> createState() => _HostState();
|
||||
}
|
||||
|
||||
/// 실제 화면: ExplorerHost · ExplorerCommands · ExplorerPaneParts 를 이 화면이 맡는다 (틀 · 안내 · 표시 막대는 단순하게)
|
||||
class _HostState extends State<_Host> implements ExplorerHost, ExplorerCommands, ExplorerPaneParts {
|
||||
late ExplorerController ctl;
|
||||
late ExplorerSelect sel;
|
||||
late ExplorerLayout layout;
|
||||
late ExplorerFlows flows;
|
||||
late ExplorerButtons buttons;
|
||||
bool _made = false;
|
||||
|
||||
void make(AppController c, {required bool rsync, required String root}) {
|
||||
ctl = ExplorerController(c, rsync: rsync, host: this)..local = [(root, 'T:')];
|
||||
sel = ExplorerSelect(ctl, clock: DateTime.now, openFile: (_, _) async {});
|
||||
layout = ExplorerLayout(ctl, context: () => context);
|
||||
flows = ExplorerFlows(ctl, sel,
|
||||
androidTrash: () => false, otherDirOf: (_) => null, otherDir: () => layout.other?.current, twoPanes: () => layout.dual || layout.split);
|
||||
buttons = ExplorerButtons(ctl, sel, this, flows);
|
||||
_made = true;
|
||||
}
|
||||
|
||||
void drop() {
|
||||
flows.dispose();
|
||||
sel.dispose();
|
||||
ctl.dispose();
|
||||
}
|
||||
|
||||
void redraw() => setState(() {});
|
||||
|
||||
@override
|
||||
bool get alive => mounted;
|
||||
@override
|
||||
void snack(String text) => ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(text)));
|
||||
@override
|
||||
void reveal(ExplorerPane pane, String path) {}
|
||||
@override
|
||||
void afterFrame(VoidCallback f) => WidgetsBinding.instance.addPostFrameCallback((_) => f());
|
||||
@override
|
||||
String? get otherDir => layout.other?.current;
|
||||
@override
|
||||
bool get dual => layout.dual;
|
||||
|
||||
@override
|
||||
double get rowHeight => layout.rowHeight();
|
||||
@override
|
||||
double get listTextScale => layout.listTextScale;
|
||||
@override
|
||||
bool get narrowSplit => layout.narrowSplit;
|
||||
@override
|
||||
bool get alwaysMarks => layout.alwaysMarks;
|
||||
@override
|
||||
Widget frame(int i, List<Widget> children) => Column(children: children);
|
||||
@override
|
||||
Widget header(ExplorerPane pane) => ExplorerPaneHeader(
|
||||
ctl: ctl,
|
||||
servers: ExplorerServers(ctl),
|
||||
pane: pane,
|
||||
android: false,
|
||||
onBookmarks: () {},
|
||||
onOpenFilter: () {},
|
||||
onCloseFilter: () {},
|
||||
onOpenRoot: () async {},
|
||||
onOpenShizuku: () async {},
|
||||
onReconnect: (_) async {},
|
||||
);
|
||||
@override
|
||||
List<Widget?> notices(ExplorerPane pane, String dir) => const [];
|
||||
@override
|
||||
Widget marksBar(ExplorerPane pane, {String? listDir}) => const SizedBox();
|
||||
@override
|
||||
void onMenu(ExplorerPane pane, FileEntry e, Offset at) {}
|
||||
@override
|
||||
List<String> dragPaths(ExplorerPane pane, FileEntry e) => flows.drops.dragPaths(pane, e);
|
||||
@override
|
||||
Future<void> onDrop(DragLoad load, int to, String dest) => flows.drops.drop(context, load, to, dest);
|
||||
@override
|
||||
void closeFilter(ExplorerPane pane) {}
|
||||
@override
|
||||
void filterChanged() => setState(() {});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Scaffold(
|
||||
body: !_made ? const SizedBox() : ExplorerPanesBody(ctl: ctl, sel: sel, layout: layout, parts: this, buttons: buttons, flows: flows));
|
||||
}
|
||||
|
|
@ -0,0 +1,255 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.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/file_tools.dart' show HexViewerPage;
|
||||
import 'package:image/image.dart' as img;
|
||||
import 'package:jj_mkvmaker/ui/reader_page.dart' show ReaderPage;
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'support/explorer_clock_keys.dart';
|
||||
import 'support/explorer_harness.dart';
|
||||
|
||||
/// E5e: 예전 explorer_test.dart 에서 나눈 탐색기 화면 시험 - 두 창 누르기 · 한 창 갈 곳 · 슬라이드쇼 · 해시 · 경로 막대 · 길게 = 짝 · 다시 누르면 접기 · 트리 맨 위 항목 · 밀어서 창 바꾸기
|
||||
void main() {
|
||||
useExplorerFixture();
|
||||
|
||||
testWidgets('두 창: 폴더 한 번 = 펼치기 · 파일 한 번 = 선택 · 두 번 = 실행 · 길게 = 메뉴 · 다른 창으로 복사', (tester) async {
|
||||
await open(tester);
|
||||
// 두 창 + 가운데 버튼 줄. 선택 표시는 선택 모드에서만 (규칙 4, 10/22 - 설정으로 늘 보이게 할 수 있음)
|
||||
expect(find.text('상위 폴더'), findsOneWidget);
|
||||
expect(find.text('doc.txt'), findsOneWidget);
|
||||
expect(find.byTooltip('표시 (여러 개 고르기)'), findsNothing);
|
||||
// 모니터링 · rsync 는 Rsync 화면에만
|
||||
expect(find.text('모니터링'), findsNothing);
|
||||
expect(find.text('좌 → 우'), findsNothing);
|
||||
|
||||
// 폴더를 한 번 누르면 바로 펼친다. 규칙 3 (10/22): 조금 뒤 다시 누르면 접고, 또 누르면 펼친다
|
||||
// (예전 189 "다시 눌러도 접지 않음" 은 규칙 3 으로 바뀜. 두 번 누르기의 두 번째는 접지 않음 - 규칙 3 시험)
|
||||
await act(tester, () => tester.tap(find.text('sub')));
|
||||
await settle(tester, () => find.text('inner.txt').evaluate().isNotEmpty);
|
||||
expect(find.text('inner.txt'), findsOneWidget);
|
||||
testNow = testNow.add(const Duration(seconds: 1)); // 두 번 누르기 시간이 지난 뒤
|
||||
await act(tester, () => tester.tap(find.text('sub')));
|
||||
await settle(tester, () => find.text('inner.txt').evaluate().isEmpty);
|
||||
expect(find.text('inner.txt'), findsNothing, reason: '규칙 3: 다시 누르면 접힘');
|
||||
testNow = testNow.add(const Duration(seconds: 1));
|
||||
await act(tester, () => tester.tap(find.text('sub')));
|
||||
await settle(tester, () => find.text('inner.txt').evaluate().isNotEmpty);
|
||||
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 맞추기 (관리자 지시 10/20): 폴더 [사진 슬라이드쇼] - 그 폴더 그림을 [계속 보기] 로 열기 · 그림이 없으면 알림', (tester) async {
|
||||
for (final n in ['b.png', 'a.png']) {
|
||||
File(p.join(left, 'sub', n)).writeAsBytesSync(img.encodePng(img.Image(width: 20, height: 30)));
|
||||
}
|
||||
Directory(p.join(left, 'empty')).createSync();
|
||||
await open(tester);
|
||||
await tester.longPress(find.text('sub'));
|
||||
await calm(tester);
|
||||
await act(tester, () => tester.tap(find.text('사진 슬라이드쇼')));
|
||||
await settle(tester, () => find.byType(ReaderPage).evaluate().isNotEmpty);
|
||||
expect(find.text('1 / 2'), findsOneWidget);
|
||||
expect(find.text('a.png'), findsOneWidget, reason: '지금 정렬 순서 (이름) 의 첫 그림부터');
|
||||
expect(find.byTooltip('계속 보기 멈추기'), findsOneWidget, reason: '[계속 보기] 를 켠 채 시작');
|
||||
await act(tester, () async => tester.state<NavigatorState>(find.byType(Navigator).first).pop());
|
||||
await calm(tester);
|
||||
// 그림이 없는 폴더
|
||||
await tester.longPress(find.text('empty'));
|
||||
await calm(tester);
|
||||
await act(tester, () => tester.tap(find.text('사진 슬라이드쇼')));
|
||||
await settle(tester, () => find.text('이 폴더에는 그림이 없습니다').evaluate().isNotEmpty);
|
||||
expect(find.byType(ReaderPage), findsNothing);
|
||||
ScaffoldMessenger.of(tester.element(find.byType(ExplorerPage))).clearSnackBars();
|
||||
await calm(tester);
|
||||
});
|
||||
|
||||
testWidgets('X-plore P3: 길게 누르기 [해시] (MD5 · SHA-256) · 폴더 [폴더 속성] · 빠른 보기 [16진]', (tester) async {
|
||||
await open(tester);
|
||||
// 해시: doc.txt = "hello"
|
||||
await tester.longPress(find.text('doc.txt'));
|
||||
await calm(tester);
|
||||
await act(tester, () => tester.tap(find.text('해시 (MD5 · SHA-256)')));
|
||||
await settle(tester, () => find.textContaining('2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824').evaluate().isNotEmpty);
|
||||
expect(find.textContaining('5d41402abc4b2a76b9719d911017c592'), findsOneWidget, reason: 'MD5');
|
||||
await act(tester, () => tester.tap(find.text('닫기').last));
|
||||
await calm(tester);
|
||||
// 폴더 속성: sub 안에 inner.txt 하나
|
||||
await tester.longPress(find.text('sub'));
|
||||
await calm(tester);
|
||||
await act(tester, () => tester.tap(find.text('폴더 속성')));
|
||||
await settle(tester, () => find.text('파일 1개 · 폴더 0개').evaluate().isNotEmpty);
|
||||
await act(tester, () => tester.tap(find.text('닫기').last));
|
||||
await calm(tester);
|
||||
// 빠른 보기 → [16진]
|
||||
await tester.longPress(find.text('doc.txt'));
|
||||
await calm(tester);
|
||||
await act(tester, () => tester.tap(find.text('미리 보기')));
|
||||
await settle(tester, () => find.text('16진').evaluate().isNotEmpty);
|
||||
await act(tester, () => tester.tap(find.text('16진')));
|
||||
await settle(tester, () => find.byType(HexViewerPage).evaluate().isNotEmpty);
|
||||
expect(find.byType(HexViewerPage), findsOneWidget);
|
||||
// 16진 보기가 앞부분을 다 읽어 파일을 놓을 때까지 (실제 시간) 기다린 뒤 닫는다 - 열린 파일이 남으면 시험 폴더를 못 지운다
|
||||
await tester.runAsync(() => Future<void>.delayed(const Duration(milliseconds: 300)));
|
||||
await tester.pump();
|
||||
await act(tester, () => tester.pageBack());
|
||||
await calm(tester);
|
||||
});
|
||||
|
||||
testWidgets('X-plore 경로 막대: 위 막대의 경로를 누르면 지금 폴더 · 위 폴더들 (창의 맨 위까지) - 고르면 그리로', (tester) async {
|
||||
await open(tester);
|
||||
await act(tester, () => tester.tap(find.text('sub')));
|
||||
await settle(tester, () => find.text('inner.txt').evaluate().isNotEmpty);
|
||||
await act(tester, () => tester.tap(find.byTooltip('위 폴더로 가기')));
|
||||
await calm(tester);
|
||||
final items = find.byType(PopupMenuItem<String>);
|
||||
final n = items.evaluate().length;
|
||||
expect(n, greaterThanOrEqualTo(2), reason: 'sub · 그 위 폴더들 (창의 맨 위까지)');
|
||||
expect(tester.widget<PopupMenuItem<String>>(items.first).enabled, isFalse, reason: '지금 폴더는 고를 수 없음');
|
||||
expect(tester.widget<PopupMenuItem<String>>(items.at(1)).value, left, reason: '바로 위 = left');
|
||||
await act(tester, () => tester.tap(items.at(1)));
|
||||
await settle(tester, () => false, rounds: 15);
|
||||
// 위로 갔으면 다시 열 때 하나 적다 (지금 폴더 = left)
|
||||
await act(tester, () => tester.tap(find.byTooltip('위 폴더로 가기')));
|
||||
await calm(tester);
|
||||
expect(find.byType(PopupMenuItem<String>), findsNWidgets(n - 1));
|
||||
});
|
||||
|
||||
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('규칙 3: 펼친 폴더를 다시 누르면 접힘 · 두 번 누르기의 두 번째는 접지 않음 (처음 값 켬)', (tester) async {
|
||||
expect(AppSettings().explorerRetapCollapse, isTrue, reason: '규칙 3 (10/22)');
|
||||
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);
|
||||
// 두 번 누르기 시간 안의 두 번째 누르기 - 접지 않음
|
||||
testNow = testNow.add(const Duration(milliseconds: 100));
|
||||
await act(tester, () => tester.tap(find.text('sub')));
|
||||
await settle(tester, () => false, rounds: 15);
|
||||
expect(find.text('inner.txt'), findsOneWidget, reason: '두 번 누르기의 두 번째');
|
||||
// 시간이 지난 뒤 다시 누름 - 접힘
|
||||
testNow = testNow.add(const Duration(seconds: 1));
|
||||
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 ⚖: 탐색기 안에서 옆으로 밀기 = 창 바꾸기 (한 창이면 숨은 창과 바꿈)', (tester) async {
|
||||
expect(AppSettings().explorerSwipePanes, isFalse, reason: '처음 값은 끔 - 화면 가운데를 밀면 화면 이동 (10/8 사용자 결정)');
|
||||
c.settings.explorerSwipePanes = true;
|
||||
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));
|
||||
});
|
||||
}
|
||||
|
|
@ -11,7 +11,7 @@ import 'package:jj_mkvmaker/services/platform_services.dart';
|
|||
import 'package:jj_mkvmaker/ui/explorer_page.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
import 'explorer_test.dart' show act, settle, calm;
|
||||
import 'support/explorer_harness.dart' show act, settle, calm;
|
||||
|
||||
/// 23: 앞의 복사가 끝나지 않아도 다음 복사를 함께 시작한다 (막지 않음, 진행 막대가 둘)
|
||||
void main() {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,447 @@
|
|||
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/settings.dart';
|
||||
import 'package:jj_mkvmaker/core/file_ops.dart' show formatSize;
|
||||
import 'package:jj_mkvmaker/ui/explorer_page.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'support/explorer_harness.dart';
|
||||
|
||||
/// E5e: 예전 explorer_test.dart 에서 나눈 탐색기 화면 시험 - 폴더 + 파일 목록 · 스타일 · 선택 모드 · 지우기 실패 · 즐겨찾기 · 고르기 도구 · 길게 누르기 메뉴 · 뒤로 · 앞으로 · 트리 화살표 폭
|
||||
void main() {
|
||||
useExplorerFixture();
|
||||
|
||||
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);
|
||||
|
||||
// 줄 끝 표시 단추: 규칙 4 - 모양은 V (Icons.check), 고른 것은 "표시 지우기"
|
||||
IconButton markOf(String name) => tester.widget<IconButton>(find.descendant(
|
||||
of: find.ancestor(of: find.text(name), matching: find.byType(Material)).first, matching: find.byType(IconButton)));
|
||||
bool markIcon(String name) {
|
||||
expect((markOf(name).icon as Icon).icon, Icons.check, reason: '규칙 4: V 모양');
|
||||
return markOf(name).tooltip == '표시 지우기';
|
||||
}
|
||||
|
||||
// 폴더: 누르면 자기 자신과 안의 것 모두 선택 (펼쳐서 ✔ 로 보여 줌)
|
||||
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(isTrue));
|
||||
// 안의 것 하나를 누르면 그것만 빠지고 나머지는 고른 채로
|
||||
await act(tester, () => tester.tap(find.text('inner.txt')));
|
||||
await settle(tester, () => false, rounds: 5);
|
||||
expect(markIcon('inner.txt'), isFalse);
|
||||
expect(markIcon('second.txt'), isTrue);
|
||||
expect(markIcon('sub'), isFalse);
|
||||
// 하나를 뺀 폴더를 다시 누르면 그 폴더와 안의 것 모두 취소 (관리자 결정 2, 10/22 - 규칙 6 "다시 누르면 전체 취소")
|
||||
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(isFalse));
|
||||
// [선택 끝] → 표시가 모두 풀리고 표시 단추도 사라진다 (규칙 4: 선택 모드에서만)
|
||||
await act(tester, () => tester.tap(find.text('선택 끝').first)); // 두 창 모두에 있음
|
||||
expect(find.byTooltip('표시 지우기'), findsNothing);
|
||||
expect(find.byTooltip('표시 (여러 개 고르기)'), findsNothing);
|
||||
// 설정 "늘 보이기" 를 켜면 X-plore 스타일은 선택 모드 밖에서도 보임
|
||||
await tester.runAsync(() => c.updateSettings((x) => x.explorerAlwaysMarks = true));
|
||||
await settle(tester, () => false, rounds: 5);
|
||||
expect(find.byTooltip('표시 (여러 개 고르기)'), findsWidgets);
|
||||
});
|
||||
|
||||
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');
|
||||
});
|
||||
}
|
||||
|
|
@ -12,7 +12,7 @@ import 'package:jj_mkvmaker/ui/explorer_page.dart';
|
|||
import 'package:path/path.dart' as p;
|
||||
import 'support/fake_recycle_bin.dart';
|
||||
|
||||
import 'explorer_test.dart' show calm;
|
||||
import 'support/explorer_harness.dart' show calm;
|
||||
/// 180: 복사 · 이동이 시작 · 진행 · 끝나도 목록의 자리와 스크롤은 그대로 (다음 것을 누르려다 엉뚱한 것을 누르지 않게)
|
||||
void main() {
|
||||
late Directory tmp;
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -13,7 +13,7 @@ import 'package:jj_mkvmaker/services/platform_services.dart';
|
|||
import 'package:jj_mkvmaker/ui/explorer_page.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
import 'explorer_test.dart' show act, doubleTap, settle, calm;
|
||||
import 'support/explorer_harness.dart' show act, doubleTap, settle, calm;
|
||||
import 'support/dav_server.dart';
|
||||
|
||||
class _Shell extends NoopShell {
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import 'package:jj_mkvmaker/services/platform_services.dart';
|
|||
import 'package:jj_mkvmaker/ui/explorer_page.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
import 'explorer_test.dart' show act, settle;
|
||||
import 'support/explorer_harness.dart' show act, settle;
|
||||
import 'support/dav_server.dart';
|
||||
import 'package:jj_mkvmaker/ui/sync_preview_view.dart' show keepOptionsTogether;
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,140 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:jj_mkvmaker/app/app_controller.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/ui/explorer_page.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
import 'explorer_clock_keys.dart';
|
||||
import 'fake_recycle_bin.dart';
|
||||
|
||||
/// E5e: 탐색기 화면 시험의 공용 준비 (예전 explorer_test.dart 의 setUp · tearDown · open · 도우미).
|
||||
/// [useExplorerFixture] 를 main 맨 앞에서 부르면 시험마다 시험 폴더 (left: doc.txt · sub/inner.txt, right: 빈 폴더) 와
|
||||
/// 앱 (다른 앱으로 열기를 적기만 하는 셸, 저장소 없음) 을 새로 만들고 끝나면 지운다. 휴지통은 가짜 (사용자 휴지통에 닿지 않게)
|
||||
|
||||
/// 다른 앱으로 열기를 기록만 하는 셸 (실제로 프로그램을 띄우지 않게)
|
||||
class ExplorerShell extends NoopShell {
|
||||
final opened = <(String, bool)>[];
|
||||
final scanned = <List<String>>[];
|
||||
@override
|
||||
Future<void> mediaScan(List<String> paths) async => scanned.add(paths);
|
||||
@override
|
||||
Future<bool> openWith(String path, {bool choose = false}) async {
|
||||
opened.add((path, choose));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
Directory? _tmp;
|
||||
String? _left, _right;
|
||||
ExplorerShell? _shell;
|
||||
AppController? _c;
|
||||
|
||||
/// 이 시험의 시험 폴더 · 왼쪽 · 오른쪽 창 폴더 · 셸 · 앱
|
||||
Directory get tmp => _tmp!;
|
||||
String get left => _left!;
|
||||
String get right => _right!;
|
||||
ExplorerShell get shell => _shell!;
|
||||
AppController get c => _c!;
|
||||
|
||||
void useExplorerFixture() {
|
||||
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 = ExplorerShell();
|
||||
_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));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// 탐색기를 1600 × 900 으로 열고 왼쪽 창 목록이 보일 때까지 기다린다 (지금 시각은 정한 시각)
|
||||
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, clock: () => testNow))));
|
||||
await settle(tester, () => find.text('doc.txt').evaluate().isNotEmpty);
|
||||
}
|
||||
|
||||
/// [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));
|
||||
}
|
||||
|
|
@ -14,7 +14,7 @@ import 'package:jj_mkvmaker/ui/explorer_page.dart';
|
|||
import 'package:jj_mkvmaker/ui/monitor_page.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
import 'explorer_test.dart' show act, calm, settle;
|
||||
import 'support/explorer_harness.dart' show act, calm, settle;
|
||||
import 'package:jj_mkvmaker/ui/sync_preview_view.dart' show keepOptionsTogether;
|
||||
|
||||
/// 68 · 70 · 71
|
||||
|
|
|
|||
Loading…
Reference in New Issue