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:image/image.dart' as img; import 'package:integration_test/integration_test.dart'; import 'package:jj_mkvmaker/app/i18n_controller.dart'; import 'package:jj_mkvmaker/core/batch_rename.dart'; import 'package:jj_mkvmaker/core/file_ops.dart'; import 'package:jj_mkvmaker/core/text_codec.dart'; import 'package:jj_mkvmaker/ui/batch_rename_dialog.dart'; import 'package:jj_mkvmaker/ui/explorer_grid.dart'; import 'package:jj_mkvmaker/ui/storage_page.dart'; import 'package:jj_mkvmaker/ui/text_viewer.dart'; import 'package:jj_mkvmaker/ui/theme.dart'; import 'support/temp_dir.dart'; import 'package:path/path.dart' as p; /// X-plore 비교 g · h · i · P3 (관리자 지시 10/18): 격자 보기 · 글 뷰어 · 일괄 이름 (날짜) · 저장 공간 그림을 PNG 로 /// (JJ_SHOT_DIR, 없으면 임시 폴더). 크기 412 · 960, 언어 ko · en. 시험 파일은 임시 폴더에만. void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); // ⑦ 밝은 화면 캡처: JJ_SHOT_THEME=light (JJ_SHOT_DIR 를 따로 두어 어두운 것과 견줌) if (Platform.environment['JJ_SHOT_THEME'] == 'light') JjColors.use(Brightness.light); final out = Platform.environment['JJ_SHOT_DIR'] ?? p.join(Directory.systemTemp.path, 'jj_shots_xptools'); for (final lang in ['ko', 'en']) { for (final (w, h) in [(412, 900), (960, 600)]) { testWidgets('X-plore 도구 $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)); // 관리자 지시 (10/24): 폴더 이름 · 시각을 실행마다 같게 (캡처가 흔들리지 않게 - 시험 쪽에서만) final tmp = testFixedTempDir('jj_xptshot_fixed'); // 그림 (색이 다른 작은 그림) · 동영상 이름 · 글 (CP949) final pics = []; for (var i = 0; i < 7; i++) { final im = img.Image(width: 64, height: 48); img.fill(im, color: img.ColorRgb8(40 + i * 30, 120, 200 - i * 20)); if (i == 0) im.exif.exifIfd['DateTimeOriginal'] = img.IfdValueAscii('2024:05:01 12:34:56'); final f = File(p.join(tmp.path, 'IMG_${1000 + i}.jpg'))..writeAsBytesSync(img.encodeJpg(im)); pics.add(f.path); } File(p.join(tmp.path, 'clip.mkv')).writeAsBytesSync(List.filled(10, 0)); final txt = File(p.join(tmp.path, 'memo.txt')) ..writeAsBytesSync(encodeText(List.generate(30, (i) => '${i + 1}번째 줄 - 한글 메모 line $i').join('\n'), 'CP949').bytes); for (final (n, size) in [('Videos', 900000), ('Photos', 400000), ('Docs', 120000), ('Music', 60000)]) { final d = Directory(p.join(tmp.path, 'disk', n))..createSync(recursive: true); File(p.join(d.path, 'big.bin')).writeAsBytesSync(List.filled(size, 0)); } File(p.join(tmp.path, 'disk', 'loose.bin')).writeAsBytesSync(List.filled(80000, 0)); pinTestTimes(tmp); final shotKey = GlobalKey(); final nav = GlobalKey(); await t.runAsync(() => t.pumpWidget(RepaintBoundary( key: shotKey, child: MaterialApp(navigatorKey: nav, theme: buildTheme(), debugShowCheckedModeBanner: false, home: const Scaffold()), ))); Future settle([int n = 12]) 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(4); final r = shotKey.currentContext!.findRenderObject()! as RenderRepaintBoundary; final image = await t.runAsync(() => r.toImage()); final bytes = await t.runAsync(() => image!.toByteData(format: ui.ImageByteFormat.png)); Directory(out).createSync(recursive: true); File(p.join(out, 'xptools_${name}_${lang}_$w.png')).writeAsBytesSync(bytes!.buffer.asUint8List()); } // g 격자 보기 final entries = [ for (final x in [...pics, p.join(tmp.path, 'clip.mkv'), txt.path]) FileEntry(x, isDir: false, size: File(x).lengthSync(), modified: DateTime(2026, 10, 18)), ]; nav.currentState!.push(MaterialPageRoute( builder: (_) => ExplorerGridPage( title: 'Photos', entries: entries, imageExts: const ['jpg'], videoThumb: (_) async => null, onOpen: (_) async {}, ), )); await settle(20); await shot('grid'); nav.currentState!.pop(); await settle(); // h 글 뷰어 (CP949 를 스스로 알아봄) nav.currentState!.push(MaterialPageRoute(builder: (_) => TextViewerPage(path: txt.path))); await settle(20); await shot('text'); nav.currentState!.pop(); await settle(); // i 일괄 이름 바꾸기 - [D] (사진은 찍은 때) showBatchRename(nav.currentContext!, pics.take(4).toList(), isDir: (_) => false, initial: const RenameRule(template: '[D]_[T]_[C]')); await settle(30); await shot('rename_date'); await t.tap(find.text(lang == 'ko' ? '취소' : 'Cancel').last); await settle(); // P3 저장 공간 그림 nav.currentState!.push(MaterialPageRoute(builder: (_) => StorageAnalysisPage(root: p.join(tmp.path, 'disk')))); await settle(30); await t.tap(find.text(lang == 'ko' ? '칸 그림' : 'Treemap')); await settle(); await shot('storage_chart'); nav.currentState!.pop(); await settle(); await t.pumpWidget(const SizedBox()); await settle(4); }); } } }