import 'dart:io'; import 'dart:ui' as ui; import 'package:archive/archive_io.dart'; 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/file_ops.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/archive_ui.dart'; import 'package:jj_mkvmaker/ui/batch_rename_dialog.dart'; import 'package:jj_mkvmaker/ui/explorer_drag.dart'; import 'package:jj_mkvmaker/ui/explorer_page.dart' show TransferProgressChip; import 'package:jj_mkvmaker/ui/explorer_search.dart'; import 'package:jj_mkvmaker/ui/quick_look.dart'; import 'package:jj_mkvmaker/ui/storage_page.dart'; import 'package:jj_mkvmaker/ui/theme.dart'; import 'package:path/path.dart' as p; /// 관리자 지시 5 (10/17): 기기 없이 15 가 볼 수 있게 묶음6 새 화면을 PNG 로 (JJ_SHOT_DIR, 없으면 임시 폴더). /// 78 이름 일괄 창 · 79 찾기 결과 · 81 빠른 보기 · 82 놓기 묻기 창 · 85 작은 표시 (보통 · 멈춤) · 86 풀기 확인 창 (경고 줄) · 87 분석 화면. /// 크기 412 (폰 세로) · 960, 언어 ko · en · ja. 시험 파일은 임시 폴더에만 (끝나면 지움), 설정은 메모리에만. void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); final out = Platform.environment['JJ_SHOT_DIR'] ?? p.join(Directory.systemTemp.path, 'jj_shots_b6'); for (final lang in ['ko', 'en', 'ja']) { for (final (w, h) in [(412, 900), (960, 600)]) { testWidgets('묶음6 화면 $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 = Directory.systemTemp.createTempSync('jj_b6shot_'); addTearDown(() => tmp.deleteSync(recursive: true)); // 시험 파일 File(p.join(tmp.path, 'Show', 'ep01.mkv')) ..createSync(recursive: true) ..writeAsBytesSync(List.filled(4000, 0)); File(p.join(tmp.path, 'Show', 'ep02.mkv')).writeAsBytesSync(List.filled(3000, 0)); File(p.join(tmp.path, 'Show', 'Show 01.mkv')).writeAsBytesSync(List.filled(10, 0)); File(p.join(tmp.path, 'Show', 'ep01.srt')).writeAsStringSync('1\n00:00:01,000 --> 00:00:02,000\n안녕 hello\n'); File(p.join(tmp.path, 'docs', 'note.txt')) ..createSync(recursive: true) ..writeAsStringSync('메모 note'); final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService())); final navKey = GlobalKey(); final shotKey = GlobalKey(); await t.pumpWidget(RepaintBoundary( key: shotKey, // DEBUG 띠가 오른쪽 위 단추 (79 [모두 선택]) 를 가리지 않게 (15 지적) child: MaterialApp( theme: buildTheme(), debugShowCheckedModeBanner: false, navigatorKey: navKey, home: const Scaffold(body: SizedBox.expand())), )); BuildContext ctx() => navKey.currentContext!; Future settle([int n = 8]) async { for (var i = 0; i < n; i++) { await t.runAsync(() => Future.delayed(const Duration(milliseconds: 60))); await t.pump(); } } Future shot(String name) async { await settle(3); final b = shotKey.currentContext!.findRenderObject()! as RenderRepaintBoundary; final img = await t.runAsync(() => b.toImage()); final bytes = await t.runAsync(() => img!.toByteData(format: ui.ImageByteFormat.png)); Directory(out).createSync(recursive: true); File(p.join(out, 'b6_${name}_${lang}_$w.png')).writeAsBytesSync(bytes!.buffer.asUint8List()); } Future closeAll() async { navKey.currentState!.popUntil((r) => r.isFirst); await settle(4); } // 78 이름 일괄 바꾸기 - 틀 "Show [C]" (첫 번째가 고르지 않은 "Show 01.mkv" 와 겹쳐 빨갛게) final eps = [p.join(tmp.path, 'Show', 'ep01.mkv'), p.join(tmp.path, 'Show', 'ep02.mkv')]; showBatchRename(ctx(), eps, isDir: (_) => false); await settle(); await t.enterText(find.byType(TextField).first, 'Show [C]'); await settle(); await shot('78_rename'); await closeAll(); // 79 찾기 결과 - 하나 골라 아래 할 일 줄까지 navKey.currentState!.push(MaterialPageRoute( builder: (_) => SearchResultsPage( root: tmp.path, cond: const SearchConditions(name: 'ep'), showHidden: false, otherDir: p.join(tmp.path, 'docs'), onOpen: (_) async {}, ), )); await settle(12); await t.tap(find.byType(Checkbox).first); await settle(); await shot('79_search'); await closeAll(); // 81 빠른 보기 - 자막 (글 미리 보기) final list = await t.runAsync(() => listEntries(p.join(tmp.path, 'Show'))); final srt = list!.indexWhere((e) => e.name == 'ep01.srt'); showDialog( context: ctx(), builder: (_) => QuickLook( entries: list, index: srt, probe: (_) async => const MediaInfo(streams: []), thumbnail: (_) async => null, imageExts: const ['jpg', 'png'], onOpen: (_) {}, ), ); await settle(10); await shot('81_quicklook'); await closeAll(); // 82 놓기 묻기 창 askDropAction(ctx(), eps, p.join(tmp.path, 'docs')); await settle(); await shot('82_drop_ask'); await closeAll(); // 85 작은 표시 - 보통 · 멈춤 for (final (name, failed) in [('85_chip', false), ('85_chip_failed', true)]) { navKey.currentState!.push(MaterialPageRoute( builder: (_) => Scaffold( body: Align( alignment: Alignment.bottomRight, child: Padding( padding: const EdgeInsets.all(8), child: TransferProgressChip(running: failed ? 0 : 2, percent: failed ? null : 45, failed: failed, onTap: () {}), ), ), ), )); await settle(); await shot(name); await closeAll(); } // 86 풀기 확인 창 - 밖으로 나가는 경로 · 링크 · 쓸 수 없는 이름 · 덮어쓰기 경고 final a = Archive() ..addFile(ArchiveFile.string('ok/a.txt', 'a')) ..addFile(ArchiveFile.string('../evil.txt', 'x')) ..addFile(ArchiveFile.string('CON.txt', 'x')) ..addFile(ArchiveFile.string('lnk', '../x')..mode = 0xA1FF); final zip = p.join(tmp.path, 'sample.zip'); File(zip).writeAsBytesSync(ZipEncoder().encode(a)); extractArchiveFlow(ctx(), c, zip); await settle(15); await shot('86_extract_confirm'); // 같은 이름: 덮어쓰기 → "되돌릴 수 없습니다" await t.tap(find.byType(DropdownButton)); await settle(); await t.tap(find.text(lang == 'ko' ? '덮어쓰기' : (lang == 'en' ? 'Overwrite' : '上書き')).last); await settle(); await shot('86_extract_overwrite'); await closeAll(); // 87 분석 화면 - 큰 폴더 · 큰 파일 navKey.currentState!.push(MaterialPageRoute(builder: (_) => StorageAnalysisPage(root: tmp.path))); await settle(15); await shot('87_storage_folders'); await t.tap(find.byType(ChoiceChip).last); await settle(); await shot('87_storage_files'); await closeAll(); }); } } }