diff --git a/test/batch_test.dart b/test/batch_test.dart index 68732f4..752a802 100644 --- a/test/batch_test.dart +++ b/test/batch_test.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:io'; import 'package:flutter/material.dart'; @@ -15,6 +16,7 @@ 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/ai_services.dart'; import 'package:jj_mkvmaker/services/downloader.dart'; +import 'package:jj_mkvmaker/services/media_tool.dart'; import 'package:jj_mkvmaker/services/model_store.dart'; import 'package:jj_mkvmaker/services/platform_services.dart'; import 'package:jj_mkvmaker/ui/app_actions.dart'; @@ -36,6 +38,35 @@ class _Recognizer implements SpeechRecognizer { } } +/// 240: 취소될 때까지 끝나지 않는 FFmpeg (여럿이 동시에 도는 상태를 만든다) +class _HoldTool implements MediaTool { + final runs = >{}; + @override + Future version() async => 'ffmpeg test'; + @override + Future> encoders() async => {'libx264'}; + @override + Future probe(String path) async => const MediaInfo(duration: Duration(seconds: 1)); + @override + Future runFfmpeg(List args, {Duration? duration, ProgressCallback? onProgress, Object? tag}) async { + final c = runs[tag] = Completer(); + try { + await c.future; + } finally { + runs.remove(tag); + } + } + + void _stop(Completer? c) { + if (c != null && !c.isCompleted) c.completeError(MediaToolException('사용자가 취소했습니다.')); + } + + @override + void cancel() => runs.values.toList().forEach(_stop); + @override + void cancelTagged(Object tag) => _stop(runs[tag]); +} + class _Translator implements Translator { @override Future load(String modelDir) async {} @@ -504,4 +535,50 @@ void main() { await tester.pumpWidget(const SizedBox()); d.dispose(); }); + + testWidgets('240: 여럿이 동시에 돌 때 ■ 는 묻는다 ([그대로 두기] · [모두 취소 (n개)]) · 목록 줄의 ■ 는 그 동영상만', (tester) async { + tester.view.physicalSize = const Size(1600, 900); + tester.view.devicePixelRatio = 1; + addTearDown(tester.view.reset); + final tool = _HoldTool(); + final dir = Directory.systemTemp.createTempSync('jj_batch240_'); + addTearDown(() => dir.deleteSync(recursive: true)); + final files = [ + for (final n in ['a', 'b']) (File(p.join(dir.path, '$n.mp4'))..writeAsBytesSync([0])).path, + ]; + final c = AppController(PlatformServices(mediaTool: tool, storage: DesktopStorageService())); + late Future building; + await tester.runAsync(() async { + await c.init(); + c.settings.maxParallelJobs = 5; + await c.addVideos(files); + building = c.buildVideos([...c.videos]); + for (var i = 0; i < 200 && tool.runs.length < 2; i++) { + await Future.delayed(const Duration(milliseconds: 10)); + } + }); + final [a, b] = c.videos; + expect([a.status, b.status], [JobStatus.running, JobStatus.running]); + await tester.pumpWidget(MaterialApp(home: HomePage(c: c))); + await tester.pump(); + // ■ (위 막대): 둘이 돌고 남은 것은 없음 → 묻는다 + await tester.tap(find.byTooltip('작업 취소').first); + await tester.pump(const Duration(milliseconds: 400)); // 도는 동안은 진행 막대가 움직여 pumpAndSettle 이 끝나지 않는다 + expect(find.text('모두 취소 (2개)'), findsOneWidget); + await tester.tap(find.text('그대로 두기')); + await tester.pump(const Duration(milliseconds: 400)); // 도는 동안은 진행 막대가 움직여 pumpAndSettle 이 끝나지 않는다 + expect(tool.runs.length, 2, reason: '[그대로 두기] 는 아무것도 취소하지 않음'); + // 목록 줄의 ■: 그 동영상만 + expect(find.byTooltip('이 동영상만 취소'), findsNWidgets(2)); + await tester.tap(find.byTooltip('이 동영상만 취소').first); + await tester.runAsync(() => Future.delayed(const Duration(milliseconds: 100))); + await tester.pump(); + expect([a.status, b.status], [JobStatus.cancelled, JobStatus.running]); + // 하나만 남으면 ■ 는 묻지 않고 바로 취소 + await tester.tap(find.byTooltip('작업 취소').first); + await tester.runAsync(() => building); + await tester.pump(); + expect(b.status, JobStatus.cancelled); + expect(find.text('모두 취소 (2개)'), findsNothing); + }); }