import 'dart:io'; import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:jj_mkvmaker/core/app_manager.dart'; import 'package:jj_mkvmaker/ui/app_manager_page.dart'; import 'package:jj_mkvmaker/ui/explorer_page.dart' show ExplorerButton; import 'package:path/path.dart' as p; /// X-plore 비교 5-2 앱 관리자 (사용자 결정 10/21, Android): 보이는 앱만 · APK 백업 · 앱 정보 · 지우기 (시스템 창) class _Fake implements AppManagerBackend { final List apps; final String dir; final calls = []; _Fake(this.apps, this.dir); @override Future> list() async => apps; @override Future icon(String package) async => null; @override Future details(String package) async { calls.add('details:$package'); return true; } @override Future uninstall(String package) async { calls.add('uninstall:$package'); return true; } @override Future backupDir() async => dir; } void main() { late Directory tmp; setUp(() => tmp = Directory.systemTemp.createTempSync('jj_apps_')); tearDown(() => tmp.deleteSync(recursive: true)); InstalledApp app(String label, {int size = 1000, int day = 1, bool system = false, int splits = 0, String? apk}) => InstalledApp( package: 'com.x.${label.toLowerCase()}', label: label, version: '1.2', apk: apk ?? '', installed: DateTime(2026, 1, day), updated: DateTime(2026, 1, day), size: size, system: system, splits: splits, ); test('기기 답 (지도) 읽기 · APK 백업 이름 (못 쓰는 글자 _ · 이미 있으면 (2))', () async { final a = InstalledApp.fromMap({'package': 'p', 'label': 'A/B', 'version': '3', 'apk': '/x', 'installed': 0, 'updated': 0, 'size': 5, 'splits': 2, 'system': true}); expect((a.label, a.size, a.splits, a.system), ('A/B', 5, 2, true)); final src = File(p.join(tmp.path, 'base.apk'))..writeAsStringSync('apk'); final b = app('A/B', apk: src.path); final out = p.join(tmp.path, 'out'); final first = await backupApk(b, out); final second = await backupApk(b, out); expect(p.basename(first), 'A_B_1.2.apk'); expect(p.basename(second), 'A_B_1.2 (2).apk'); expect(File(second).readAsStringSync(), 'apk'); }); testWidgets('화면: 안내 · 이름 / 크기 / 설치일 정렬 · 찾기 · [APK 백업] · [앱 정보] · 시스템 앱은 [지우기] 없음', (tester) async { tester.view.physicalSize = const Size(900, 1400); tester.view.devicePixelRatio = 1; addTearDown(tester.view.reset); final src = File(p.join(tmp.path, 'b.apk'))..writeAsStringSync('apk'); final fake = _Fake([ app('Banana', size: 300, day: 5, apk: src.path, splits: 3), app('Apple', size: 900, day: 1), app('Settings', size: 100, day: 2, system: true), ], p.join(tmp.path, 'Download', 'JJ_MKVMaker', 'apk')); await tester.pumpWidget(MaterialApp(home: AppManagerPage(backend: fake))); await tester.pumpAndSettle(); expect(find.text('보이는 앱만 나옵니다 (모든 앱 목록 권한은 쓰지 않음)'), findsOneWidget); double y(String t) => tester.getTopLeft(find.text(t)).dy; expect(y('Apple') < y('Banana') && y('Banana') < y('Settings'), isTrue, reason: '처음은 이름 순'); await tester.tap(find.text('이름')); await tester.pumpAndSettle(); await tester.tap(find.text('크기').last); await tester.pumpAndSettle(); expect(y('Apple') < y('Banana'), isTrue, reason: '큰 것부터'); await tester.enterText(find.byType(TextField), 'ban'); await tester.pump(); expect(find.text('Apple'), findsNothing); // APK 백업 (나눠진 앱이면 알림) await tester.tap(find.text('Banana')); await tester.pumpAndSettle(); expect(find.text('지우기 (시스템 창에서 확인)'), findsOneWidget); await tester.tap(find.text('APK 백업')); for (var i = 0; i < 20; i++) { await tester.runAsync(() => Future.delayed(const Duration(milliseconds: 20))); await tester.pump(); } expect(File(p.join(fake.dir, 'Banana_1.2.apk')).existsSync(), isTrue); expect(find.textContaining('나눠진 앱 (조각 3개)'), findsOneWidget); // 시스템 앱: 지우기 없음 · 앱 정보는 시스템 화면 await tester.enterText(find.byType(TextField), 'set'); await tester.pump(); await tester.tap(find.text('Settings')); await tester.pumpAndSettle(); expect(find.text('지우기 (시스템 창에서 확인)'), findsNothing); await tester.tap(find.text('앱 정보 (시스템 화면)')); await tester.pumpAndSettle(); expect(fake.calls, ['details:com.x.settings']); }); test('가운데 버튼 [앱 관리자] 는 Android 에서만 고를 수 있음', () { // 설계 M4: Android 인지는 부르는 쪽이 넘긴다 (c.services.platform.isAndroid) expect(ExplorerButton.explorerValues(android: false), isNot(contains(ExplorerButton.apps))); expect(ExplorerButton.explorerValues(android: true), contains(ExplorerButton.apps)); }); }