JJ_MKVMaker/test/explorer_test.dart

254 lines
12 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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/app/settings.dart';
import 'package:jj_mkvmaker/platform/windows/desktop_storage_service.dart';
import 'package:jj_mkvmaker/platform/windows/process_media_tool.dart';
import 'package:jj_mkvmaker/services/app_shell.dart';
import 'package:jj_mkvmaker/services/platform_services.dart';
import 'package:jj_mkvmaker/ui/explorer_page.dart';
import 'package:path/path.dart' as p;
/// 다른 앱으로 열기를 기록만 하는 셸 (실제로 프로그램을 띄우지 않게)
class _Shell extends NoopShell {
final opened = <(String, bool)>[];
@override
Future<bool> openWith(String path, {bool choose = false}) async {
opened.add((path, choose));
return true;
}
}
void main() {
late Directory tmp;
late String left, right;
late _Shell shell;
late AppController c;
setUp(() {
tmp = Directory.systemTemp.createTempSync('jj_explorer_');
left = p.join(tmp.path, 'left');
right = p.join(tmp.path, 'right');
File(p.join(left, 'doc.txt'))
..createSync(recursive: true)
..writeAsStringSync('hello');
File(p.join(left, 'sub', 'inner.txt'))
..createSync(recursive: true)
..writeAsStringSync('inner');
Directory(right).createSync();
shell = _Shell();
c = AppController(PlatformServices(
mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService(), shell: shell));
c.settings.explorerPaths = [left, right];
});
tearDown(() => tmp.deleteSync(recursive: true));
Future<void> open(WidgetTester tester) async {
tester.view.physicalSize = const Size(1600, 900);
tester.view.devicePixelRatio = 1;
addTearDown(tester.view.reset);
await tester.runAsync(() => tester.pumpWidget(MaterialApp(home: ExplorerPage(c: c))));
await settle(tester, () => find.text('doc.txt').evaluate().isNotEmpty);
}
testWidgets('두 창 (선택 모드): 한 번 = 선택 · 화살표 = 펼치기 · 두 번 = 실행 · 길게 = 메뉴 · 다른 창으로 복사', (tester) async {
await open(tester);
// 두 창 + 가운데 버튼 줄
expect(find.text('상위 폴더'), findsOneWidget);
expect(find.text('doc.txt'), findsOneWidget);
// 폴더를 한 번 누르면 고르기만 (펼치지 않음)
await act(tester, () => tester.tap(find.text('sub')));
await settle(tester, () => false, rounds: 15);
expect(find.text('inner.txt'), findsNothing);
// › 화살표로 펼친다
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 tester.pumpAndSettle();
expect(find.text('다른 창으로 복사'), 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 tester.pumpAndSettle();
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('바로 실행 모드: 폴더를 누르면 펼치고, 파일을 누르면 바로 실행', (tester) async {
c.settings.explorerClick = 'open';
await open(tester);
await act(tester, () => tester.tap(find.text('sub')));
await settle(tester, () => find.text('inner.txt').evaluate().isNotEmpty);
expect(find.text('inner.txt'), findsOneWidget);
await act(tester, () => tester.tap(find.text('doc.txt')));
await settle(tester, () => shell.opened.isNotEmpty);
expect(shell.opened.single, (p.join(left, 'doc.txt'), false));
});
testWidgets('폴더 + 파일 목록 배치 · Windows 탐색기 / Total Commander 스타일', (tester) async {
c.settings
..explorerLayout = 'split'
..explorerStyle = 'windows';
await open(tester);
// 왼쪽 트리는 폴더만, 오른쪽 목록은 고른 폴더 (left) 의 내용
expect(find.text('sub'), findsNWidgets(2)); // 트리 + 목록
expect(find.text('doc.txt'), findsOneWidget); // 목록에만
expect(find.text('수정한 날짜'), findsOneWidget); // Windows 탐색기 열 머리
expect(find.text('TXT 파일'), findsOneWidget);
expect(find.text('상위 폴더'), findsOneWidget); // 버튼 줄은 두 창 사이
// 왼쪽 트리에서 sub 를 고르면 오른쪽 목록이 그 폴더로
await act(tester, () => tester.tap(find.text('sub').first));
await settle(tester, () => find.text('inner.txt').evaluate().isNotEmpty);
expect(find.text('inner.txt'), findsOneWidget);
expect(find.text('doc.txt'), findsNothing);
// 목록의 ".." 을 누르면 상위 폴더로
await act(tester, () => tester.tap(find.text('..')));
await settle(tester, () => find.text('doc.txt').evaluate().isNotEmpty);
expect(find.text('doc.txt'), findsOneWidget);
// 목록의 폴더를 두 번 누르면 들어간다
await doubleTap(tester, find.text('sub').last);
await settle(tester, () => find.text('inner.txt').evaluate().isNotEmpty);
expect(find.text('inner.txt'), findsOneWidget);
// Total Commander: [폴더] · <DIR> · 확장자 칸
await act(tester, () => tester.tap(find.text('..')));
await settle(tester, () => find.text('doc.txt').evaluate().isNotEmpty);
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'), findsOneWidget); // 확장자 칸
expect(find.text('확장자'), findsOneWidget);
});
testWidgets('여러 개 표시 → 삭제 · 새 폴더 · 이름 변경', (tester) async {
await open(tester);
// 동그라미로 doc.txt · sub 표시 (반드시 이름으로 찾은 줄의 버튼만: 목록에는 시험 폴더 위쪽의 실제 폴더도 보인다)
await act(tester, () => tester.tap(markOf('sub')));
await act(tester, () => tester.tap(markOf('doc.txt')));
// 표시 버튼은 두 번 누르기를 기다리지 않고 바로
expect(find.text('2개 표시함'), findsOneWidget);
await act(tester, () => tester.tap(find.text('삭제').first));
await tester.pumpAndSettle();
// 지울 항목 이름이 확인 창에 보인다
expect(find.textContaining('2개 항목을 지울까요?'), findsOneWidget);
expect(find.textContaining('· sub'), findsOneWidget);
expect(find.textContaining('· doc.txt'), findsOneWidget);
await act(tester, () => tester.tap(find.widgetWithText(FilledButton, '삭제')));
await settle(tester, () => find.text('doc.txt').evaluate().isEmpty);
expect(Directory(left).listSync(), isEmpty);
// 새 폴더 → 이름 변경
await act(tester, () => tester.tap(find.text('새 폴더').first));
await tester.pumpAndSettle();
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 act(tester, () => tester.tap(find.text('이름 변경').first));
await tester.pumpAndSettle();
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.text('창 배치').first);
await tester.pumpAndSettle();
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 tester.pumpAndSettle();
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 tester.pumpAndSettle();
await tester.tap(find.byTooltip('창 배치 · 버튼 구성'));
await tester.pumpAndSettle();
await tester.tap(find.text('버튼 구성').last);
await tester.pumpAndSettle();
await tester.tap(find.widgetWithText(CheckboxListTile, 'MKV 목록에 추가'));
await tester.pump();
await tester.tap(find.widgetWithText(FilledButton, '확인'));
await tester.pumpAndSettle();
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]);
});
}
/// [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 {
await tester.runAsync(() async {
await tester.tap(f);
await Future<void>.delayed(const Duration(milliseconds: 80));
await tester.tap(f);
});
await tester.pump();
}
/// 실제 파일 작업 (비동기 IO) 이 끝나기를 기다리며 화면을 갱신
Future<void> settle(WidgetTester tester, bool Function() done, {int rounds = 100}) 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));
}