import 'dart:async'; import 'dart:io'; import 'dart:ui' as ui; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; import 'package:jj_mkvmaker/app/app_controller.dart'; import 'package:jj_mkvmaker/app/i18n_controller.dart'; import 'package:jj_mkvmaker/core/models.dart'; import 'package:jj_mkvmaker/l10n/tr.dart'; import 'package:jj_mkvmaker/platform/windows/desktop_storage_service.dart'; import 'package:jj_mkvmaker/services/media_tool.dart'; import 'package:jj_mkvmaker/services/platform_services.dart'; import 'package:jj_mkvmaker/ui/home_page.dart'; import 'package:jj_mkvmaker/ui/theme.dart'; import 'package:path/path.dart' as p; import 'support/temp_dir.dart'; /// 시험이 끝내라고 할 때까지 FFmpeg 이 도는 척 (끝나면 임시 출력 파일을 씀) - test/batch_test.dart 의 _EndTool 과 같음 class _EndTool 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); } File(args.last).writeAsStringSync('NEW'); } void _stop(Completer? c) { if (c != null && !c.isCompleted) c.completeError(MediaToolException('사용자가 취소했습니다.')); } void finish(Object tag) => runs[tag]!.complete(); @override void cancel() => runs.values.toList().forEach(_stop); @override void cancelTagged(Object tag) => _stop(runs[tag]); } /// 262 (관리자 지시 10/21): MKV 묶음에서 한 동영상이 끝나는 바로 그때 - 그 줄의 ■ 자리에 오는 ⟳ 는 묶음이 끝날 때까지 눌리지 않고 /// (✕ 도 밀려오지 않음), 끝나면 둘 다 성공. ko · en · ja × 412 · 960 (가짜 FFmpeg, 시험 폴더 안) void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); final out = Platform.environment['JJ_SHOT_DIR'] ?? ''; for (final lang in ['ko', 'en', 'ja']) { for (final (w, h) in [(412, 900), (960, 600)]) { testWidgets('262 MKV 끝나는 순간 $lang ${w}x$h', (t) async { t.view.physicalSize = Size(w.toDouble(), h.toDouble()); t.view.devicePixelRatio = 1; addTearDown(t.view.reset); await t.runAsync(() => i18n.apply(lang, save: false)); addTearDown(() => i18n.apply('ko', save: false)); final tmp = testTempDir('jj_mkvend_'); appTempInto(tmp); final files = [for (final n in ['episode_01', 'episode_02']) (File(p.join(tmp.path, '$n.mp4'))..writeAsBytesSync([0])).path]; final tool = _EndTool(); final c = AppController(PlatformServices(mediaTool: tool, storage: DesktopStorageService())); late Future building; await t.runAsync(() async { await c.init(); c.settings.mkv.maxParallelJobs = 1; await c.addVideos(files); building = c.buildVideos([...c.videos]); for (var i = 0; i < 200 && tool.runs.isEmpty; i++) { await Future.delayed(const Duration(milliseconds: 10)); } }); final [a, b] = c.videos; final shotKey = GlobalKey(); await t.pumpWidget(RepaintBoundary( key: shotKey, child: MaterialApp(theme: buildTheme(), debugShowCheckedModeBanner: false, home: HomePage(c: c)), )); Future settle([int n = 6]) async { for (var i = 0; i < n; i++) { await t.runAsync(() => Future.delayed(const Duration(milliseconds: 50))); await t.pump(); } } Future shot(String name) async { await settle(2); expect(t.takeException(), isNull, reason: '$name 넘침'); if (out.isEmpty) return; final r = shotKey.currentContext!.findRenderObject()! as RenderRepaintBoundary; final img = await t.runAsync(() => r.toImage()); final bytes = await t.runAsync(() => img!.toByteData(format: ui.ImageByteFormat.png)); Directory(out).createSync(recursive: true); File(p.join(out, 'mkvend_${name}_${lang}_$w.png')).writeAsBytesSync(bytes!.buffer.asUint8List()); } await settle(); final stop = find.byTooltip(tr('이 동영상만 취소')); expect(stop, findsWidgets, reason: '도는 줄의 ■'); final stopAt = t.getCenter(stop.first); await shot('1_running'); // a 가 끝나고 b 가 시작 - a 의 ■ 자리를 누른다 (손가락이 ■ 를 누르려던 것) await t.runAsync(() async { tool.finish(a); for (var i = 0; i < 200 && !tool.runs.containsKey(b); i++) { await Future.delayed(const Duration(milliseconds: 10)); } }); await settle(); expect(a.status, JobStatus.done); await t.tapAt(stopAt); await settle(); expect(a.status, JobStatus.done, reason: '⟳ 로 대기가 되지 않음'); expect(c.videos, contains(a), reason: '✕ 로 빠지지 않음'); await shot('2_a_done_b_running'); await t.runAsync(() async { tool.finish(b); await building; }); await settle(); expect((a.status, b.status), (JobStatus.done, JobStatus.done)); await shot('3_all_done'); await t.pumpWidget(const SizedBox()); await t.pump(const Duration(seconds: 2)); }); } } }