import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:jj_mkvmaker/core/storage_scan.dart'; import 'package:jj_mkvmaker/ui/storage_page.dart'; import 'package:path/path.dart' as p; /// 87: 저장 공간 분석 - 폴더마다 크기 (안쪽 합) · 큰 파일 차례, 링크는 세지 않음 (80 과 같게) · 취소, 화면은 들어가기 · 그 자리로 void main() { late Directory tmp; setUp(() { tmp = Directory.systemTemp.createTempSync('jj_scan87_'); File(p.join(tmp.path, 'movies', 'a.mkv')) ..createSync(recursive: true) ..writeAsBytesSync(List.filled(5000, 0)); File(p.join(tmp.path, 'movies', 'old', 'b.mkv')) ..createSync(recursive: true) ..writeAsBytesSync(List.filled(3000, 0)); File(p.join(tmp.path, 'docs', 'c.txt')) ..createSync(recursive: true) ..writeAsBytesSync(List.filled(100, 0)); File(p.join(tmp.path, 'top.bin')).writeAsBytesSync(List.filled(10, 0)); }); tearDown(() => tmp.deleteSync(recursive: true)); test('폴더마다 안쪽 합 · 큰 폴더 차례 · 큰 파일 차례', () async { final s = await StorageScanner().scan(tmp.path); expect(s.sizeOf(tmp.path), 8110); expect(s.filesOf(tmp.path), 4); expect(s.sizeOf(p.join(tmp.path, 'movies')), 8000); expect(s.biggestIn(tmp.path).map(p.basename), ['movies', 'docs']); expect(s.bigFiles.map((f) => f.name).take(3), ['a.mkv', 'b.mkv', 'c.txt']); }); test('링크 · junction 은 세지 않고 (위 폴더를 가리키는 고리도 끝남) · 취소', () async { var junctions = 0; if (Platform.isWindows) { for (final (link, target) in [(p.join(tmp.path, 'docs', 'loop'), tmp.path), (p.join(tmp.path, 'tomovies'), p.join(tmp.path, 'movies'))]) { if ((await Process.run('cmd', ['/c', 'mklink', '/J', link, target])).exitCode == 0) junctions++; } } final s = await StorageScanner().scan(tmp.path); expect(s.sizeOf(tmp.path), 8110, reason: 'junction 이 가리키는 것을 더하지 않는다'); expect(s.links, junctions); await expectLater(StorageScanner().scan(tmp.path, cancelled: () => true), throwsA(isA())); for (final l in [p.join(tmp.path, 'docs', 'loop'), p.join(tmp.path, 'tomovies')]) { if (Link(l).existsSync()) Link(l).deleteSync(); } }); testWidgets('화면: 큰 폴더 → 들어가기 · 위로 · 큰 파일 · [그 자리로] 는 그 경로를 돌려준다', (tester) async { tester.view.physicalSize = const Size(1000, 900); tester.view.devicePixelRatio = 1; addTearDown(tester.view.reset); final key = GlobalKey(); await tester.pumpWidget(MaterialApp(navigatorKey: key, home: const Scaffold())); String? picked; var done = false; Navigator.of(key.currentContext!) .push(MaterialPageRoute(builder: (_) => StorageAnalysisPage(root: tmp.path))) .then((v) { picked = v; done = true; }); for (var i = 0; i < 200 && find.text('movies').evaluate().isEmpty; i++) { await tester.runAsync(() => Future.delayed(const Duration(milliseconds: 20))); await tester.pump(); } expect(find.text('movies'), findsOneWidget); await tester.tap(find.text('movies')); await tester.pump(); expect(find.text('old'), findsOneWidget, reason: '들어가기'); await tester.tap(find.byTooltip('위 폴더')); await tester.pump(); await tester.tap(find.text('큰 파일')); await tester.pump(); expect(find.text('a.mkv'), findsOneWidget); await tester.tap(find.text('a.mkv')); await tester.pumpAndSettle(); expect(done, isTrue); expect(picked, p.join(tmp.path, 'movies', 'a.mkv')); }); test('5-1 ⑤ 지운 것을 다시 훑지 않고 빼기: 폴더 (그 아래 모두) · 파일 - 위 폴더들의 크기 · 파일 수에서 덜어 냄', () async { final s = await StorageScanner().scan(tmp.path); final movies = p.join(tmp.path, 'movies'); s.remove(p.join(movies, 'old')); expect(s.sizeOf(movies), 5000); expect(s.filesOf(movies), 1); expect(s.sizeOf(tmp.path), 5110); expect(s.biggestIn(movies), isEmpty); expect(s.bigFiles.map((f) => f.name), isNot(contains('b.mkv'))); s.remove(p.join(movies, 'a.mkv')); expect((s.sizeOf(movies), s.filesOf(movies)), (0, 0)); expect((s.sizeOf(tmp.path), s.filesOf(tmp.path)), (110, 2)); // 큰 파일 목록에 없는 파일은 알려 준 크기로 s.remove(p.join(tmp.path, 'unknown.bin'), fileSize: 10); expect(s.sizeOf(tmp.path), 100); }); testWidgets('5-1 ⑤ 화면: ⋮ [지우기 (확인 · 휴지통)] 는 탐색기의 지우기를 부르고 결과에서 뺌 · 취소면 그대로 · [이동 (붙여넣기로)] 는 담기', (tester) async { tester.view.physicalSize = const Size(1000, 900); tester.view.devicePixelRatio = 1; addTearDown(tester.view.reset); final asked = >[]; final cut = >[]; var confirm = false; await tester.pumpWidget(MaterialApp( home: StorageAnalysisPage( root: tmp.path, onDelete: (paths) async { asked.add(paths); if (!confirm) return const []; for (final x in paths) { Directory(x).deleteSync(recursive: true); } return paths; }, onCut: cut.add, ), )); for (var i = 0; i < 200 && find.text('movies').evaluate().isEmpty; i++) { await tester.runAsync(() => Future.delayed(const Duration(milliseconds: 20))); await tester.pump(); } Future menu(String row, String item) async { await tester.tap(find.descendant(of: find.widgetWithText(ListTile, row), matching: find.byTooltip('더 보기'))); await tester.pumpAndSettle(); await tester.tap(find.text(item).last); for (var i = 0; i < 10; i++) { await tester.runAsync(() => Future.delayed(const Duration(milliseconds: 20))); await tester.pump(); } } final movies = p.join(tmp.path, 'movies'); // 취소 (탐색기 확인 창에서 [취소]) → 그대로 await menu('movies', '지우기 (확인 · 휴지통)'); expect(asked.single, [movies]); expect(find.text('movies'), findsOneWidget); // 확인 → 목록에서 빠지고 위 폴더 크기도 줄어듦 confirm = true; await menu('movies', '지우기 (확인 · 휴지통)'); expect(find.text('movies'), findsNothing); expect(find.textContaining('110B'), findsWidgets); // 이동 (붙여넣기로) await menu('docs', '이동 (붙여넣기로)'); expect(cut.single, [p.join(tmp.path, 'docs')]); await tester.pumpWidget(const SizedBox()); }); }