JJ_MKVMaker/test/move_test.dart

234 lines
11 KiB
Dart

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/core/languages.dart';
import 'package:jj_mkvmaker/core/models.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/home_page.dart';
import 'package:path/path.dart' as p;
AppController _controller() =>
AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
void main() {
late Directory dir;
setUp(() => dir = Directory.systemTemp.createTempSync('jj_move_'));
tearDown(() => dir.deleteSync(recursive: true));
File make(String name) => File(p.join(dir.path, name))
..parent.createSync(recursive: true)
..writeAsStringSync(name);
test('이동: 동영상 + 옆의 같은 이름 자막을 옮기고 목록에서 뺀다. 같은 이름이 있으면 (2)', () async {
final c = _controller();
final dest = p.join(dir.path, 'done');
final a = VideoItem(make('a.mp4').path)
..subtitles.add(SubtitleEntry.external(path: make('a.ko.srt').path, language: undetermined))
..subtitles.add(SubtitleEntry.external(path: make('other.srt').path, language: undetermined));
final b = VideoItem(make('b.mkv').path);
final busy = VideoItem(make('c.mp4').path)..status = JobStatus.running;
make(p.join('done', 'b.mkv')); // 이미 있는 이름
// 만든 결과물 (jj_mkv): a 의 것만 함께 옮긴다
make(p.join('jj_mkv', 'a.mkv'));
make(p.join('jj_mkv', 'a_AI.srt'));
make(p.join('jj_mkv', 'a_zh-Hans.srt'));
make(p.join('jj_mkv', 'ab.mkv'));
make(p.join('jj_mkv', 'a_1_ko.srt'));
c.videos.addAll([a, b, busy]);
c.selected = a;
final (moved, errors) = await c.moveVideos([a, b, busy], dest);
expect(moved, 2);
expect(errors.single, contains('작업 중'));
expect(File(p.join(dest, 'a.mp4')).existsSync(), isTrue);
expect(File(p.join(dest, 'a.ko.srt')).existsSync(), isTrue);
expect(File(p.join(dir.path, 'other.srt')).existsSync(), isTrue, reason: '이름이 다른 자막은 그대로');
expect(File(p.join(dest, 'b (2).mkv')).readAsStringSync(), 'b.mkv');
expect(File(p.join(dir.path, 'a.mp4')).existsSync(), isFalse);
for (final f in ['a.mkv', 'a_AI.srt', 'a_zh-Hans.srt']) {
expect(File(p.join(dest, 'jj_mkv', f)).existsSync(), isTrue, reason: f);
expect(File(p.join(dir.path, 'jj_mkv', f)).existsSync(), isFalse, reason: f);
}
for (final f in ['ab.mkv', 'a_1_ko.srt']) {
expect(File(p.join(dir.path, 'jj_mkv', f)).existsSync(), isTrue, reason: '다른 동영상의 결과물 $f 는 그대로');
}
expect(c.videos, [busy]);
expect(c.selected, busy);
});
test('옮기기 시작하면 바로 목록에서 빠지고, 옮기지 못하면 원래 자리 · 표시 · 선택으로 돌아온다', () async {
final c = _controller();
final dest = p.join(dir.path, 'done');
final a = VideoItem(make('a.mp4').path);
final b = VideoItem(make('b.mp4').path);
final d = VideoItem(make('d.mp4').path);
c.videos.addAll([a, b, d]);
c.checked.add(b);
c.selected = b;
// 처음 알릴 때 (옮기는 중) 이미 목록에 없다
List<String>? whileMoving;
void first() => whileMoving ??= c.videos.map((v) => v.fileName).toList();
c.addListener(first);
// Windows: 열려 있는 파일은 옮길 수 없다
final h = File(b.path).openSync(mode: FileMode.append);
try {
final (moved, errors) = await c.moveVideos([b], dest);
expect(moved, 0);
expect(errors.single, contains('b.mp4'));
} finally {
h.closeSync();
c.removeListener(first);
}
expect(whileMoving, ['a.mp4', 'd.mp4']);
expect(c.videos, [a, b, d]);
expect(c.checked, {b});
expect(c.selected, b);
expect(File(b.path).existsSync(), isTrue);
expect(File(p.join(dest, 'b.mp4')).existsSync(), isFalse, reason: '옮기다 만 사본을 남기지 않는다');
}, skip: !Platform.isWindows);
test('함께 옮기는 자막 · 결과물을 옮기지 못하면 원본은 그대로 두고 대상에 사본을 남기지 않는다', () async {
final c = _controller();
final dest = p.join(dir.path, 'done');
final a = VideoItem(make('a.mp4').path)
..subtitles.add(SubtitleEntry.external(path: make('a.ko.srt').path, language: undetermined));
make(p.join('jj_mkv', 'a.mkv'));
c.videos.add(a);
// Windows: 열려 있는 파일은 옮길 수 없다 (옆 자막 · jj_mkv 결과물)
final h1 = File(p.join(dir.path, 'a.ko.srt')).openSync(mode: FileMode.append);
final h2 = File(p.join(dir.path, 'jj_mkv', 'a.mkv')).openSync(mode: FileMode.append);
try {
final (moved, _) = await c.moveVideos([a], dest);
expect(moved, 1, reason: '동영상은 옮긴다');
} finally {
h1.closeSync();
h2.closeSync();
}
expect(File(p.join(dest, 'a.mp4')).existsSync(), isTrue);
expect(File(p.join(dir.path, 'a.ko.srt')).existsSync(), isTrue);
expect(File(p.join(dest, 'a.ko.srt')).existsSync(), isFalse, reason: '자막 사본이 두 벌로 남지 않는다');
expect(File(p.join(dir.path, 'jj_mkv', 'a.mkv')).existsSync(), isTrue);
expect(File(p.join(dest, 'jj_mkv', 'a.mkv')).existsSync(), isFalse, reason: '결과물 사본이 두 벌로 남지 않는다');
}, skip: !Platform.isWindows);
test('목록에 다시 넣으면 (앱을 다시 켜도) 전에 만든 자막 (jj_mkv 의 파일명_ko.srt 등) 이 다시 붙는다', () async {
final c = _controller();
final video = make('강의.mp4').path;
make(p.join('jj_mkv', '강의_AI.srt'));
make(p.join('jj_mkv', '강의_ko.srt'));
make(p.join('jj_mkv', '강의_en.srt'));
make(p.join('jj_mkv', '다른영상_ko.srt'));
make('강의.ja.srt'); // 같은 폴더 자막
await c.addVideos([video]);
final v = c.videos.single;
expect([for (final s in v.subtitles) p.basename(s.path!)]..sort(), ['강의.ja.srt', '강의_en.srt', '강의_ko.srt']);
expect(v.subtitles.firstWhere((s) => s.path!.endsWith('_ko.srt')).language.code, 'ko');
});
test('이동 폴더를 안 정했으면 옮기지 않는다', () async {
final c = _controller();
final a = VideoItem(make('a.mp4').path);
c.videos.add(a);
final (moved, errors) = await c.moveVideos([a], '');
expect(moved, 0);
expect(errors.single, contains('이동할 폴더'));
expect(File(a.path).existsSync(), isTrue);
});
test('이동 버튼 설정 저장 (표시 이름 · 폴더 · 순서), 예전 이동 폴더 하나는 "이동" 버튼으로', () {
final s = AppSettings()
..moveTargets = const [MoveTarget('완료', r'D:\done'), MoveTarget('보관', r'E:\archive')];
expect(AppSettings.fromJson(s.toJson()).moveTargets, s.moveTargets);
expect(AppSettings().moveTargets, isEmpty);
expect(AppSettings.fromJson({'moveTargetDir': r'D:\old'}).moveTargets, const [MoveTarget('이동', r'D:\old')]);
});
testWidgets('MKV 목록: 세 번 누르면 첫 번째 이동 버튼으로 · 세부 정보의 이동 버튼 (표시 이름) 은 그 폴더로', (tester) async {
tester.view.physicalSize = const Size(1600, 900);
tester.view.devicePixelRatio = 1;
addTearDown(tester.view.reset);
final c = _controller();
final dest = p.join(dir.path, 'done');
final archive = p.join(dir.path, 'archive');
c.settings.moveTargets = [MoveTarget('완료함', dest), MoveTarget('보관함', archive)];
final a = VideoItem(make('a.mp4').path);
final b = VideoItem(make('b.mp4').path);
final d = VideoItem(make('d.mp4').path);
c.videos.addAll([a, b, d]);
c.selected = a;
await tester.pumpWidget(MaterialApp(home: HomePage(c: c, onExit: () {})));
// 파일 옮기기 (실제 입출력) 가 끝나도록 실제 시간을 조금씩 흘려 보낸다
Future<void> settleIo() async {
for (var i = 0; i < 10; i++) {
await tester.runAsync(() => Future<void>.delayed(const Duration(milliseconds: 50)));
await tester.pump(const Duration(milliseconds: 100));
}
}
// b 줄을 세 번 → b 만 이동
final tile = find.text('b.mp4').first;
for (var i = 0; i < 3; i++) {
await tester.tap(tile);
await tester.pump(const Duration(milliseconds: 80));
}
await tester.pump(const Duration(milliseconds: 400));
await tester.pumpAndSettle();
// 47: 옮기기 전에 묻는다 - [취소] 면 그대로, [옮기기] 면 이동
expect(find.text('파일 옮기기'), findsOneWidget);
expect(find.textContaining('"b.mp4" 을(를) "완료함" 폴더로 옮길까요?'), findsOneWidget);
await tester.tap(find.text('취소'));
await tester.pumpAndSettle();
await settleIo();
expect(File(p.join(dest, 'b.mp4')).existsSync(), isFalse);
for (var i = 0; i < 3; i++) {
await tester.tap(tile);
await tester.pump(const Duration(milliseconds: 80));
}
await tester.pump(const Duration(milliseconds: 400));
await tester.pumpAndSettle();
await tester.tap(find.widgetWithText(FilledButton, '옮기기'));
await tester.pump();
await settleIo();
// 옮긴 뒤 목록에서 빠지는 것은 조금 늦다 (전체 시험 중 바쁠 때) - 빠질 때까지 기다린다
for (var i = 0; i < 30 && c.videos.any((v) => v.fileName == 'b.mp4'); i++) {
await settleIo();
}
expect(File(p.join(dest, 'b.mp4')).existsSync(), isTrue);
// 102: 세 번 누르기를 "아무것도 안 함" 으로 하면 묻지도 옮기지도 않는다
c.settings.tripleTapAction = 'none';
final dTile = find.text('d.mp4').first;
for (var i = 0; i < 3; i++) {
await tester.tap(dTile);
await tester.pump(const Duration(milliseconds: 80));
}
await tester.pump(const Duration(milliseconds: 400));
await tester.pumpAndSettle();
expect(find.text('파일 옮기기'), findsNothing);
expect(File(p.join(dest, 'd.mp4')).existsSync(), isFalse);
c.settings.tripleTapAction = 'move';
expect(c.videos.map((v) => v.fileName), ['a.mp4', 'd.mp4']);
// 세부 정보 오른쪽 아래: 표시 이름 버튼 두 개 (첫 번째가 아래, 두 번째가 그 위)
c.select(a);
await tester.pump();
expect(find.text('완료함'), findsOneWidget);
expect(find.text('보관함'), findsOneWidget);
expect(find.text('이동'), findsNothing);
expect(tester.getCenter(find.text('보관함')).dy, lessThan(tester.getCenter(find.text('완료함')).dy));
// 체크 없음: [보관함] → 보고 있는 동영상 (a) 를 보관 폴더로
await tester.tap(find.text('보관함'));
await settleIo();
expect(File(p.join(archive, 'a.mp4')).existsSync(), isTrue);
expect(c.videos.map((v) => v.fileName), ['d.mp4']);
// 체크하면 버튼에 개수
c.toggleChecked(d);
await tester.pump();
expect(find.text('완료함 (1)'), findsOneWidget);
});
}