JJ_MKVMaker/test/dismiss_test.dart

455 lines
26 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/services.dart' show SystemChannels;
import 'package:flutter_test/flutter_test.dart';
import 'package:jj_mkvmaker/app/app_controller.dart';
import 'package:jj_mkvmaker/app/copy_center.dart';
import 'package:jj_mkvmaker/app/live_sync.dart';
import 'package:jj_mkvmaker/app/settings.dart';
import 'package:jj_mkvmaker/app/transfer_job.dart' show userErrorText;
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/confirm.dart';
import 'package:jj_mkvmaker/ui/explorer_page.dart';
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 'support/explorer_harness.dart' show act, settle;
/// 110: 확인 창은 뒤로 가기 · 바깥 누르기 · [취소] 로 닫으면 아무것도 하지 않는다 ([실행] · [지우기] 등만 실행)
void main() {
const ways = ['back', 'barrier', 'cancel'];
Future<void> dismiss(WidgetTester tester, String how) async {
switch (how) {
case 'back':
await tester.runAsync(() => tester.binding.handlePopRoute());
case 'barrier':
await tester.tapAt(const Offset(4, 4));
default:
await tester.tap(find.text('취소').last);
}
await tester.pumpAndSettle();
}
late Directory tmp;
late AppController c;
setUp(() {
tmp = Directory.systemTemp.createTempSync('jj_dismiss_');
c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
c.settings.explorer.style = 'xplore'; // E6: 이 시험은 X-plore 모양 (트리) 을 본다 - Windows 처음 값은 Total Commander
});
tearDown(() => tmp.deleteSync(recursive: true));
/// [run] 을 누르는 단추 하나만 있는 화면
Future<void> host(WidgetTester tester, Future<void> Function(BuildContext) run) async {
tester.view.physicalSize = const Size(1400, 900);
tester.view.devicePixelRatio = 1;
addTearDown(tester.view.reset);
await tester.pumpWidget(MaterialApp(
home: Scaffold(body: Builder(builder: (ctx) => Center(child: ElevatedButton(onPressed: () => run(ctx), child: const Text('열기'))))),
));
await tester.tap(find.text('열기'));
await tester.pump(const Duration(milliseconds: 400));
}
for (final how in ways) {
testWidgets('46 · 47 공용 확인 창 ($how) → false', (tester) async {
bool? r;
await host(tester, (ctx) async => r = await confirmAction(ctx, title: 't', body: 'b', ok: '지우기'));
expect(find.text('지우기'), findsOneWidget);
await dismiss(tester, how);
expect(r, isFalse);
});
testWidgets('104 지우기 미리 보기 창 ($how) → 실행 안 함', (tester) async {
final src = p.join(tmp.path, 'src'), dst = p.join(tmp.path, 'dst');
File(p.join(src, 'a.txt'))
..createSync(recursive: true)
..writeAsStringSync('a');
File(p.join(dst, 'old.txt'))
..createSync(recursive: true)
..writeAsStringSync('o');
bool? r;
final t = CopyTask(id: '1', sources: [src], dest: dst, contents: true, method: 'rsync', options: '-av --delete');
await host(tester, (ctx) async => r = await confirmDeletingRun(ctx, t));
await settle(tester, () => find.textContaining('비교하는 중').evaluate().isEmpty, rounds: 200);
expect(find.textContaining('− old.txt'), findsOneWidget);
await dismiss(tester, how);
expect(r, isFalse);
});
testWidgets('42 지울 목록 창 ($how) → 지우지도 · 지우기를 끄지도 않음', (tester) async {
final src = p.join(tmp.path, 's'), dst = p.join(tmp.path, 'd');
File(p.join(src, 'a.txt'))
..createSync(recursive: true)
..writeAsStringSync('a');
File(p.join(dst, 'only.txt'))
..createSync(recursive: true)
..writeAsStringSync('o');
final pair = LiveSyncPair(src, dst, delete: true);
c.settings.copy.liveSyncPairs = [pair];
final live = LiveSync(c);
addTearDown(live.dispose);
await tester.runAsync(() => live.syncNow(pair));
expect(live.toDelete[LiveSync.keyOf(pair)], ['only.txt']);
await host(tester, (ctx) => confirmLiveDeletes(ctx, live, c.settings.copy.liveSyncPairs.single));
await dismiss(tester, how == 'cancel' ? 'back' : how); // 이 창의 "닫기" 는 [나중에]
await settle(tester, () => false, rounds: 10);
expect(File(p.join(dst, 'only.txt')).existsSync(), isTrue);
expect(c.settings.copy.liveSyncPairs.single.delete, isTrue);
expect(c.settings.copy.liveSyncPairs.single.deleteConfirmed, isFalse);
});
testWidgets('70 원본이 빈 쌍의 [그래도 맞추기] 창 ($how) → 지우지 않음', (tester) async {
final src = p.join(tmp.path, 'e'), dst = p.join(tmp.path, 'f');
Directory(src).createSync();
File(p.join(dst, 'keep.txt'))
..createSync(recursive: true)
..writeAsStringSync('k');
final pair = LiveSyncPair(src, dst, delete: true).copyWith(deleteConfirmed: true);
c.settings.copy.liveSyncPairs = [pair];
final live = LiveSync(c);
addTearDown(live.dispose);
await tester.runAsync(() => live.syncNow(pair));
await host(tester, (ctx) => confirmEmptySourceSync(ctx, live, pair));
await dismiss(tester, how);
await settle(tester, () => false, rounds: 10);
expect(File(p.join(dst, 'keep.txt')).existsSync(), isTrue);
});
testWidgets('65 탐색기 지우기 창 ($how) → 그대로', (tester) async {
final dir = p.join(tmp.path, 'x');
final f = File(p.join(dir, 'doc.txt'))
..createSync(recursive: true)
..writeAsStringSync('d');
c.settings
..explorer.layout = 'single'
..explorer.paths = [dir, dir];
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);
await act(tester, () => tester.tap(find.text('doc.txt').first));
await settle(tester, () => false, rounds: 10);
await act(tester, () => tester.tap(find.text('삭제').first));
await tester.pumpAndSettle();
await dismiss(tester, how);
await settle(tester, () => false, rounds: 10);
expect(f.existsSync(), isTrue);
});
}
testWidgets('104: 모니터링 [실행] 도 지우기가 있으면 미리 보기를 거친다 (취소하면 실행 안 함)', (tester) async {
final src = p.join(tmp.path, 'ms'), dst = p.join(tmp.path, 'md');
File(p.join(src, 'a.txt'))
..createSync(recursive: true)
..writeAsStringSync('a');
final keep = File(p.join(dst, 'keep.txt'))
..createSync(recursive: true)
..writeAsStringSync('k');
c.settings.copy.tasks = [CopyTask(id: '1', sources: [src], dest: dst, contents: true, method: 'rsync', options: '-av --delete')];
tester.view.physicalSize = const Size(1500, 1000);
tester.view.devicePixelRatio = 1;
addTearDown(tester.view.reset);
await tester.pumpWidget(MaterialApp(home: MonitorPage(c: c)));
await tester.pump();
await tester.tap(find.widgetWithText(FilledButton, '실행').first);
await tester.pump(const Duration(milliseconds: 300));
expect(find.text('지우기가 들어 있는 작업입니다'), findsOneWidget);
await settle(tester, () => find.textContaining('비교하는 중').evaluate().isEmpty, rounds: 200);
expect(find.textContaining('− keep.txt'), findsOneWidget);
await tester.tap(find.text('취소').last);
await tester.pumpAndSettle();
await settle(tester, () => false, rounds: 10);
expect(keep.existsSync(), isTrue);
expect(find.text('아직 실행 안 함'), findsOneWidget);
});
testWidgets('210: 모니터링 [실행] 의 이동 (원본 지우기) 도 미리 보기 · 확인을 거친다 (취소하면 원본 그대로) · 복사는 바로', (tester) async {
final src = p.join(tmp.path, 'mv'), dst = p.join(tmp.path, 'mvd');
final a = File(p.join(src, 'a.txt'))
..createSync(recursive: true)
..writeAsStringSync('a');
Directory(dst).createSync();
c.settings.copy.tasks = [CopyTask(id: '1', sources: [src], dest: dst, contents: true, move: true, method: 'builtin')];
tester.view.physicalSize = const Size(1500, 1000);
tester.view.devicePixelRatio = 1;
addTearDown(tester.view.reset);
await tester.pumpWidget(MaterialApp(home: MonitorPage(c: c)));
await tester.pump();
await tester.tap(find.widgetWithText(FilledButton, '실행').first);
await tester.pump(const Duration(milliseconds: 300));
expect(find.text('원본을 지우는 작업입니다 (이동)'), findsOneWidget);
expect(find.text('이동: 대상에 옮긴 파일은 원본에서 지워집니다'), findsOneWidget);
await settle(tester, () => find.textContaining('비교하는 중').evaluate().isEmpty, rounds: 200);
expect(find.textContaining('a.txt'), findsWidgets, reason: '옮길 목록');
await tester.tap(find.text('취소').last);
await tester.pumpAndSettle();
await settle(tester, () => false, rounds: 10);
expect(a.existsSync(), isTrue, reason: '취소하면 원본 그대로');
expect(File(p.join(dst, 'a.txt')).existsSync(), isFalse);
expect(find.text('아직 실행 안 함'), findsOneWidget);
});
testWidgets('230: 이동 확인 창의 명령 줄은 실제로 실행할 것 (rsync 는 --remove-source-files 를 붙이고, 복사면 원본 지우기 옵션을 뺀다)', (tester) async {
final src = p.join(tmp.path, 'r'), dst = p.join(tmp.path, 'rd');
File(p.join(src, 'a.txt'))
..createSync(recursive: true)
..writeAsStringSync('a');
Directory(dst).createSync();
bool? r;
final mv = CopyTask(id: '1', sources: [src], dest: dst, contents: true, move: true, method: 'rsync', options: '-avPog');
await host(tester, (ctx) async => r = await confirmDeletingRun(ctx, mv, includeMove: true));
await settle(tester, () => find.textContaining('비교하는 중').evaluate().isEmpty, rounds: 200);
expect(find.text('rsync -avPog --remove-source-files'), findsOneWidget);
expect(find.text('rsync -avPog'), findsNothing);
await tester.tap(find.text('취소').last);
await tester.pumpAndSettle();
expect(r, isFalse);
// 복사 (지우기 있음) 인데 칸에 원본 지우기 옵션이 남아 있으면 - 실제로는 빼고 실행하므로 창에도 빼고
final cp = CopyTask(id: '2', sources: [src], dest: dst, contents: true, method: 'rsync', options: '-avPog --delete --remove-source-files');
await host(tester, (ctx) async => r = await confirmDeletingRun(ctx, cp));
await settle(tester, () => find.textContaining('비교하는 중').evaluate().isEmpty, rounds: 200);
expect(find.text('rsync -avPog --delete'), findsOneWidget);
await tester.tap(find.text('취소').last);
await tester.pumpAndSettle();
});
testWidgets('관리자 지시 (10/20): [실행] 이 막힐 때는 막히는 이유 한 줄만 - "미리 볼 수 없습니다" · [알고 실행합니다] 는 숨김', (tester) async {
final src = p.join(tmp.path, 'bs'), dst = p.join(tmp.path, 'bd');
File(p.join(src, 'a.txt'))
..createSync(recursive: true)
..writeAsStringSync('a');
Directory(dst).createSync(recursive: true);
bool? r;
// 미리 볼 수 없는 규칙 (merge) + 알아볼 수 없는 낱말 (delete) - 실행은 막힘
final t = CopyTask(id: '9', sources: [src], dest: dst, contents: true, method: 'rsync', options: '-av --delete --filter="merge C:/jj_rules/rules.txt" delete');
await host(tester, (ctx) async => r = await confirmDeletingRun(ctx, t));
await settle(tester, () => find.textContaining('비교하는 중').evaluate().isEmpty, rounds: 200);
expect(find.textContaining('옵션을 알아볼 수 없습니다'), findsOneWidget);
expect(find.textContaining('미리 볼 수 없습니다'), findsNothing);
expect(find.text('알고 실행합니다 (지울 목록이 실제와 다를 수 있음)'), findsNothing);
expect(tester.widget<FilledButton>(find.widgetWithText(FilledButton, '실행')).onPressed, isNull);
await tester.tap(find.text('취소').last);
await tester.pumpAndSettle();
expect(r, isFalse);
});
testWidgets('177 · 15 검토 F2: 미리 볼 수 없는 거르기 규칙 + --delete 면 빨간 경고 · 체크해야 [실행] 이 켜진다', (tester) async {
final src = p.join(tmp.path, 'fs'), dst = p.join(tmp.path, 'fd');
File(p.join(src, 'a.txt'))
..createSync(recursive: true)
..writeAsStringSync('a');
File(p.join(dst, 'old.txt'))
..createSync(recursive: true)
..writeAsStringSync('o');
bool? r;
// 관리자 지시 (10/20): H 는 이제 미리 볼 수 있다 - 아직 따르지 못하는 merge 규칙으로
final t = CopyTask(id: '1', sources: [src], dest: dst, contents: true, method: 'rsync', options: '-av --delete --filter="merge C:/jj_rules/rules.txt"'); // 목록 파일은 전체 경로 (관리자 판단 10/20)
await host(tester, (ctx) async => r = await confirmDeletingRun(ctx, t));
await settle(tester, () => find.textContaining('비교하는 중').evaluate().isEmpty, rounds: 200);
expect(find.textContaining('미리 볼 수 없습니다'), findsOneWidget);
expect(find.textContaining('merge C:/jj_rules/rules.txt'), findsWidgets);
FilledButton run() => tester.widget<FilledButton>(find.widgetWithText(FilledButton, '실행'));
expect(run().onPressed, isNull, reason: '체크 전에는 실행할 수 없음');
await tester.tap(find.text('알고 실행합니다 (지울 목록이 실제와 다를 수 있음)'));
await tester.pump();
expect(run().onPressed, isNotNull);
await tester.tap(find.text('취소').last);
await tester.pumpAndSettle();
expect(r, isFalse);
// 따를 수 있는 규칙만 있으면 경고 · 체크 없음
final ok = CopyTask(id: '2', sources: [src], dest: dst, contents: true, method: 'rsync', options: '-av --delete --exclude=*.log');
await host(tester, (ctx) async => r = await confirmDeletingRun(ctx, ok));
await settle(tester, () => find.textContaining('비교하는 중').evaluate().isEmpty, rounds: 200);
expect(find.textContaining('미리 볼 수 없습니다'), findsNothing);
expect(run().onPressed, isNotNull);
await tester.tap(find.text('취소').last);
await tester.pumpAndSettle();
});
testWidgets('236: 모니터링 카드 [실행] 뒤 옵션 칸에 포커스가 남지 않는다 (키보드가 저절로 올라와 끝 알림을 가리지 않게)', skip: !Platform.isWindows, (tester) async {
final src = p.join(tmp.path, 'cp'), dst = p.join(tmp.path, 'cpd');
File(p.join(src, 'a.txt'))
..createSync(recursive: true)
..writeAsStringSync('a');
Directory(dst).createSync();
c.settings.copy.tasks = [CopyTask(id: '1', sources: [src], dest: dst, contents: true, method: 'robocopy', options: '/E')];
tester.view.physicalSize = const Size(1500, 1000);
tester.view.devicePixelRatio = 1;
addTearDown(tester.view.reset);
await tester.pumpWidget(MaterialApp(home: MonitorPage(c: c)));
await tester.pump();
await tester.showKeyboard(find.widgetWithText(TextField, '/E')); // 옵션 칸을 눌러 고치는 중 (현재 방식은 칸이 꺼져 있어 robocopy)
await tester.pump();
expect(tester.widget<EditableText>(find.byType(EditableText).first).focusNode.hasFocus, isTrue);
// 고치지 않았으니 묻지 않고 바로 (복사, 지우기 없음)
await tester.tap(find.widgetWithText(FilledButton, '실행').first);
await tester.pump();
expect(tester.widget<EditableText>(find.byType(EditableText).first).focusNode.hasFocus, isFalse);
CopyCenter.of(c).jobs['1']?.cancel();
await settle(tester, () => CopyCenter.of(c).jobs['1']?.finished ?? true, rounds: 200);
});
testWidgets('237: 모니터링 카드 실패 글은 예외 이름 없이, 원문 (명령 줄 포함) 은 [자세히] 에서 보고 복사', (tester) async {
const raw = 'ProcessException: 원본 파일을 지우지 못했습니다\n Command: rsync -avPog --remove-source-files a/ b/';
c.settings.copy.tasks = [
const CopyTask(id: '1', sources: ['a'], dest: 'b', method: 'rsync', options: '-avPog', move: true)
.copyWith(lastResult: 'failed', lastRun: DateTime(2026, 10, 12, 9).toIso8601String(), lastMessage: '원본 파일을 지우지 못했습니다', lastDetail: raw),
];
expect(CopyTask.fromJson(c.settings.copy.tasks.single.toJson()).lastDetail, raw);
tester.view.physicalSize = const Size(1500, 1000);
tester.view.devicePixelRatio = 1;
addTearDown(tester.view.reset);
await tester.pumpWidget(MaterialApp(home: MonitorPage(c: c)));
await tester.pump();
expect(find.text('원본 파일을 지우지 못했습니다'), findsOneWidget);
expect(find.textContaining('ProcessException'), findsNothing);
await tester.tap(find.text('자세히'));
await tester.pumpAndSettle();
expect(find.textContaining('Command: rsync -avPog --remove-source-files'), findsOneWidget);
String? copied;
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.platform, (call) async {
if (call.method == 'Clipboard.setData') copied = (call.arguments as Map)['text'] as String?;
return null;
});
addTearDown(() => tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.platform, null));
await tester.tap(find.text('복사'));
await tester.pumpAndSettle();
expect(copied, raw);
});
test('237: 사용자에게 보이는 실패 글에는 예외 이름 · 명령 줄이 없다', () {
expect(userErrorText(const ProcessException('rsync', ['-a'], '원본 파일을 지우지 못했습니다', 23)), '원본 파일을 지우지 못했습니다');
expect(userErrorText(const FileSystemException('지울 수 없음', r'C:\x', OSError('액세스가 거부되었습니다', 5))), r'지울 수 없음 · C:\x · 액세스가 거부되었습니다');
expect(userErrorText(Exception('끊김')), '끊김');
// 245: 앱이 직접 할 때의 Dart 파일 오류 (영어) 도 rsync 때와 같은 글 - 원본을 못 지웠으면
expect(userErrorText(const FileSystemException('Cannot delete file', r'C:\s\a.txt', OSError('사용 중', 32)), sources: [r'C:\s']),
r'원본 파일을 지우지 못했습니다 (권한이 없거나 사용 중). 대상에는 복사됐고 원본은 남아 있습니다. · C:\s\a.txt · 사용 중');
expect(userErrorText(const FileSystemException('Cannot delete file', r'D:\t\x.txt'), sources: [r'C:\s']).startsWith('지우지 못했습니다'), isTrue);
expect(userErrorText(const FileSystemException('Cannot copy file to D:\\t', r'C:\s\a.txt')).startsWith('파일을 복사하지 못했습니다'), isTrue);
expect(userErrorText('그냥 글'), '그냥 글');
});
testWidgets('243: 이동 카드를 lsync 로 옮길 때 "원본을 지우지 않음 (복사로 맞춤)" 을 알리고, 취소하면 옮기지 않는다', (tester) async {
final src = p.join(tmp.path, 'mv243'), dst = p.join(tmp.path, 'mv243d');
Directory(src).createSync();
Directory(dst).createSync();
c.settings.copy.tasks = [CopyTask(id: '1', sources: [src], dest: dst, contents: true, move: true, method: 'builtin')];
tester.view.physicalSize = const Size(1500, 1000);
tester.view.devicePixelRatio = 1;
addTearDown(tester.view.reset);
await tester.pumpWidget(MaterialApp(home: MonitorPage(c: c)));
await tester.pump();
await tester.tap(find.text('lsync 로 이동').first);
await tester.pumpAndSettle();
expect(find.textContaining('실시간 동기화는 원본을 지우지 않습니다 (복사로 맞춤)'), findsOneWidget);
await tester.tap(find.text('취소').last);
await tester.pumpAndSettle();
expect(c.settings.copy.liveSyncPairs, isEmpty, reason: '취소하면 옮기지 않음');
await tester.tap(find.text('lsync 로 이동').first);
await tester.pumpAndSettle();
await tester.tap(find.widgetWithText(FilledButton, '옮기기'));
await tester.pumpAndSettle();
expect(c.settings.copy.liveSyncPairs.map((x) => x.source), [src]);
});
testWidgets('105: 원본이 대상 안 + 창에서 [원본 파일 지우기] 로 지우기 옵션이 생기면 막는다', (tester) async {
final outer = p.join(tmp.path, 'O'), inner = p.join(outer, 'in');
File(p.join(inner, 'n.txt'))
..createSync(recursive: true)
..writeAsStringSync('n');
final center = CopyCenter.of(c);
c.settings
..copy.rsyncOptions = '-avPog'
..explorer.layout = 'single'
..copy.rsyncPaths = [outer, tmp.path];
// 이동 (원본 파일 지우기) 작업만 --delete 를 기억
await center.remember([inner], outer, contents: true, method: 'rsync', move: true).then((t) => center.update(t.copyWith(options: '-avPog --delete')));
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('in').evaluate().isNotEmpty && find.text('O').evaluate().isNotEmpty);
await act(tester, () => tester.tap(find.text('in').first));
await act(tester, () => tester.tap(find.text('O').last));
await settle(tester, () => false, rounds: 5);
await act(tester, () => tester.tap(find.text('좌 → 우')));
// 창이 뜨고 비교가 끝날 때까지 (전체 시험 중 바쁠 때는 창이 늦게 뜬다 - 뜨기 전에도 "비교하는 중" 은 없다)
await settle(tester, () => find.text('rsync').evaluate().isNotEmpty && find.textContaining('비교하는 중').evaluate().isEmpty,
rounds: 300);
expect(find.text('rsync'), findsWidgets);
// 222: 원본이 대상 안 - 복사면 "복사됩니다 · 원본 폴더 그대로", 이동이면 "옮겨집니다 · 원본 파일은 지워짐"
expect(find.textContaining('바로 아래에 복사됩니다'), findsOneWidget);
await tester.tap(find.text(keepOptionsTogether('원본 파일 지우기 (--remove-source-files)')));
await tester.pump();
expect(find.textContaining('바로 아래로 옮겨집니다 (원본 파일은 지워지고'), findsOneWidget);
expect(find.textContaining('바로 아래에 복사됩니다'), findsNothing);
expect(find.text('원본이 대상 안에 있으면 지우기 (--delete) 를 함께 쓸 수 없습니다 (대상의 다른 파일이 모두 지워집니다).'), findsOneWidget);
expect(tester.widget<FilledButton>(find.widgetWithText(FilledButton, '이동')).onPressed, isNull);
await tester.tap(find.text('취소').last);
await tester.pumpAndSettle();
});
testWidgets('209: 모니터링 카드는 실제로 실행할 것을 이동 / 복사와 함께 보인다 (복사 카드 칸의 원본 지우기 옵션은 쓰지 않는다고 알림)', (tester) async {
expect(effectiveRunText('rsync', '-avPog', move: true), 'rsync -avPog --remove-source-files');
expect(effectiveRunText('rsync', '-avPog --remove-source-files', move: false), 'rsync -avPog');
expect(effectiveRunText('robocopy', '/E /R:2', move: false), 'robocopy /E /R:2 /XJ'); // 링크 · junction 은 복사도
expect(effectiveRunText('robocopy', '/E /MOV', move: true), 'robocopy /E /MOVE /IS /IT /IM /XJ'); // 238: 실제로 붙는 것까지
expect(effectiveRunText('robocopy', '/E', move: true, skipNewer: true), 'robocopy /E /MOVE /IS /IT /IM /XJ /XO'); // 229
final a = p.join(tmp.path, 'ca'), b = p.join(tmp.path, 'cb');
Directory(a).createSync(recursive: true);
Directory(b).createSync(recursive: true);
c.settings.copy.tasks = [
CopyTask(id: '1', sources: [a], dest: b, contents: true, method: 'rsync', options: '-avPog --remove-source-files'),
CopyTask(id: '2', sources: [b], dest: a, contents: true, move: true, method: 'rsync', options: '-avPog'),
];
tester.view.physicalSize = const Size(1500, 1200);
tester.view.devicePixelRatio = 1;
addTearDown(tester.view.reset);
await tester.pumpWidget(MaterialApp(home: MonitorPage(c: c)));
await tester.pump();
expect(find.text('복사 · 실제 실행: rsync -avPog'), findsOneWidget);
expect(find.textContaining('복사 작업이라 칸의 원본 지우기 옵션'), findsOneWidget);
expect(find.text('이동 · 실제 실행: rsync -avPog --remove-source-files'), findsOneWidget);
});
testWidgets('112 · 114: 모니터링 작업 - 짧은 경로, 저장하지 않은 고침이 있으면 [실행] 할 때 묻는다 (저장하면 그 옵션 · 지우기면 미리 보기)',
(tester) async {
final src = p.join(tmp.path, 'Download', 'movies'), dst = p.join(tmp.path, 'backup', 'movies');
File(p.join(src, 'a.txt'))
..createSync(recursive: true)
..writeAsStringSync('a');
File(p.join(dst, 'keep.txt'))
..createSync(recursive: true)
..writeAsStringSync('k');
c.settings.copy.tasks = [CopyTask(id: '1', sources: [src], dest: dst, contents: true, method: 'rsync', options: '-av')];
tester.view.physicalSize = const Size(1500, 1000);
tester.view.devicePixelRatio = 1;
addTearDown(tester.view.reset);
await tester.pumpWidget(MaterialApp(home: MonitorPage(c: c)));
await tester.pump();
expect(find.text('… › ${p.basename(tmp.path)} › Download › movies'), findsOneWidget); // 112
await tester.enterText(find.widgetWithText(TextField, '옵션'), '-av --delete');
await tester.pump();
await tester.tap(find.widgetWithText(FilledButton, '실행').first);
await tester.pumpAndSettle();
expect(find.text('고친 옵션을 저장하지 않았습니다'), findsOneWidget);
await tester.tap(find.text('저장하고 실행'));
await tester.pump(const Duration(milliseconds: 300));
expect(c.settings.copy.tasks.single.options, '-av --delete');
// 저장한 옵션에 지우기가 있으므로 미리 보기 창
expect(find.text('지우기가 들어 있는 작업입니다'), findsOneWidget);
await settle(tester, () => find.textContaining('비교하는 중').evaluate().isEmpty, rounds: 200);
expect(find.textContaining('− keep.txt'), findsOneWidget);
await tester.tap(find.text('취소').last);
await tester.pumpAndSettle();
expect(File(p.join(dst, 'keep.txt')).existsSync(), isTrue);
});
}