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 'explorer_test.dart' show act, settle; /// 110: 확인 창은 뒤로 가기 · 바깥 누르기 · [취소] 로 닫으면 아무것도 하지 않는다 ([실행] · [지우기] 등만 실행) void main() { const ways = ['back', 'barrier', 'cancel']; Future 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())); }); tearDown(() => tmp.deleteSync(recursive: true)); /// [run] 을 누르는 단추 하나만 있는 화면 Future host(WidgetTester tester, Future 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.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.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.liveSyncPairs.single.delete, isTrue); expect(c.settings.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.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 ..explorerLayout = 'single' ..explorerPaths = [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.copyTasks = [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.copyTasks = [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('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 rules.txt"'); 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 rules.txt'), findsWidgets); FilledButton run() => tester.widget(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.copyTasks = [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(find.byType(EditableText).first).focusNode.hasFocus, isTrue); // 고치지 않았으니 묻지 않고 바로 (복사, 지우기 없음) await tester.tap(find.widgetWithText(FilledButton, '실행').first); await tester.pump(); expect(tester.widget(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.copyTasks = [ 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.copyTasks.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.copyTasks = [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.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.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 ..rsyncOptions = '-avPog' ..explorerLayout = 'single' ..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(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.copyTasks = [ 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.copyTasks = [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.copyTasks.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); }); }