import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:jj_mkvmaker/core/file_ops.dart'; import 'package:jj_mkvmaker/ui/explorer_search.dart'; import 'package:path/path.dart' as p; /// 79: 찾기 조건 (크기 · 바뀐 날 · 종류) · 결과 목록 화면 (2000개마다 멈춤 · 더 보기, 고르고 복사 · 이동 · 지우기, 누르면 열기) void main() { final now = DateTime(2026, 10, 17, 12); FileEntry f(String name, {int size = 0, int daysAgo = 0}) => FileEntry('/x/$name', isDir: false, size: size, modified: now.subtract(Duration(days: daysAgo))); test('조건: "1GB 넘는 30일 지난 영상"', () { const gb = 1024 * 1024 * 1024; const c = SearchConditions(minBytes: gb, olderDays: 30, kind: 'video'); expect(c.accepts(f('a.mkv', size: 2 * gb, daysAgo: 40), now: now), isTrue); expect(c.accepts(f('a.mkv', size: gb ~/ 2, daysAgo: 40), now: now), isFalse, reason: '작다'); expect(c.accepts(f('a.mkv', size: 2 * gb, daysAgo: 3), now: now), isFalse, reason: '최근'); expect(c.accepts(f('a.srt', size: 2 * gb, daysAgo: 40), now: now), isFalse, reason: '동영상 아님'); expect(c.accepts(FileEntry('/x/d', isDir: true, modified: DateTime(2000)), now: now), isFalse, reason: '조건이 있으면 폴더는 아님'); expect(c.summary, contains('1.0GB')); }); test('조건: N일 안 · 확장자 직접 · 조건 없음', () { expect(const SearchConditions(newerDays: 7).accepts(f('a', daysAgo: 3), now: now), isTrue); expect(const SearchConditions(newerDays: 7).accepts(f('a', daysAgo: 9), now: now), isFalse); const custom = SearchConditions(kind: 'custom', exts: '*.MKV, .mp4 webm'); expect(custom.accepts(f('a.mkv'), now: now), isTrue); expect(custom.accepts(f('a.webm'), now: now), isTrue); expect(custom.accepts(f('a.avi'), now: now), isFalse); expect(const SearchConditions(name: 'x').hasFileConditions, isFalse); expect(const SearchConditions().isEmpty, isTrue); }); test('79-c outermostPaths: 고른 폴더 안의 것 · 같은 것을 빼고 차례는 그대로', () { final a = p.join('C:', 'x', 'a'); final inA = p.join(a, 'f.txt'); final b2 = p.join('C:', 'x', 'b.txt'); expect(outermostPaths([inA, b2, a, a]), [b2, a]); expect(outermostPaths([b2]), [b2]); expect(outermostPaths([p.join('C:', 'x', 'ab'), a]), [p.join('C:', 'x', 'ab'), a], reason: '이름 앞부분만 같은 것은 안이 아님'); }); group('searchFiles + accept', () { late Directory tmp; setUp(() { tmp = Directory.systemTemp.createTempSync('jj_search79_'); File(p.join(tmp.path, 'a', 'ep1.mkv')) ..createSync(recursive: true) ..writeAsBytesSync(List.filled(3000, 0)); File(p.join(tmp.path, 'a', 'ep1.srt')).writeAsStringSync('x'); File(p.join(tmp.path, 'small.mkv')).writeAsBytesSync(List.filled(10, 0)); }); tearDown(() => tmp.deleteSync(recursive: true)); test('이름 없이 조건만 · 이름 + 조건', () async { const c = SearchConditions(minBytes: 1000, kind: 'video'); expect(await searchFiles(tmp.path, '', accept: c.accepts).map((e) => e.name).toList(), ['ep1.mkv']); expect(await searchFiles(tmp.path, 'ep1', accept: const SearchConditions(kind: 'subtitle').accepts).map((e) => e.name).toList(), ['ep1.srt']); expect(await searchFiles(tmp.path, '').toList(), isEmpty, reason: '이름도 조건도 없으면 찾지 않는다 (예전 그대로)'); }); }); group('결과 화면', () { late Directory tmp; setUp(() { tmp = Directory.systemTemp.createTempSync('jj_search79_ui_'); for (var i = 0; i < 5; i++) { File(p.join(tmp.path, 'ep$i.mkv')).writeAsStringSync('$i'); } Directory(p.join(tmp.path, 'ep_dir')).createSync(); }); tearDown(() => tmp.deleteSync(recursive: true)); Future<(List, SearchAction? Function())> show(WidgetTester tester, {int chunk = 2000}) async { tester.view.physicalSize = const Size(900, 900); tester.view.devicePixelRatio = 1; addTearDown(tester.view.reset); final opened = []; SearchAction? result; final nav = GlobalKey(); await tester.pumpWidget(MaterialApp(navigatorKey: nav, home: const Scaffold(body: Text('탐색기')))); nav.currentState! .push(MaterialPageRoute( builder: (_) => SearchResultsPage( root: tmp.path, cond: const SearchConditions(name: 'ep'), showHidden: false, otherDir: p.join(tmp.path, 'dest'), chunk: chunk, onOpen: (e) async => opened.add(e), ), )) .then((r) => result = r); for (var i = 0; i < 40; i++) { await tester.runAsync(() => Future.delayed(const Duration(milliseconds: 20))); await tester.pump(); } return (opened, () => result); } testWidgets('2개마다 멈추고 [2개 더 보기] - 끝까지', (tester) async { await show(tester, chunk: 2); expect(find.text('찾기 결과 · 2개'), findsOneWidget); expect(find.text('2개에서 멈췄습니다'), findsOneWidget); // 79-d: 다 찾기 전에는 "지금까지 찾은 n개 모두" expect(find.text('지금까지 찾은 2개 모두'), findsOneWidget); await tester.tap(find.text('2개 더 보기')); for (var i = 0; i < 20; i++) { await tester.runAsync(() => Future.delayed(const Duration(milliseconds: 20))); await tester.pump(); } expect(find.text('찾기 결과 · 4개'), findsOneWidget); await tester.tap(find.text('2개 더 보기')); for (var i = 0; i < 20; i++) { await tester.runAsync(() => Future.delayed(const Duration(milliseconds: 20))); await tester.pump(); } expect(find.text('찾기 결과 · 6개'), findsOneWidget, reason: '파일 5 + 폴더 1'); expect(find.textContaining('멈췄습니다'), findsNothing); expect(find.widgetWithText(TextButton, '모두'), findsOneWidget, reason: '다 찾으면 그냥 "모두"'); }); testWidgets('누르면 열기 (화면 그대로) · 폴더는 그 자리로 · 고르면 복사 → 다른 창 · 지우기', (tester) async { final (opened, result) = await show(tester); await tester.tap(find.text('ep1.mkv')); await tester.pump(); expect(opened.single.name, 'ep1.mkv'); expect(find.text('찾기 결과 · 6개'), findsOneWidget, reason: '열어도 결과 화면은 그대로'); // 고르면 아래에 할 일 줄 (개수 · 크기), 고르는 동안 누르기 = 고르기 expect(find.text('지우기'), findsNothing); await tester.longPress(find.text('ep2.mkv')); await tester.pump(); await tester.tap(find.text('ep3.mkv')); await tester.pump(); expect(opened, hasLength(1), reason: '고르는 중에는 열지 않음'); expect(find.text('파일 2개 · 2B'), findsOneWidget); expect(find.text('복사 → dest'), findsOneWidget); await tester.tap(find.text('지우기')); await tester.pumpAndSettle(); expect(result()!.kind, 'delete'); expect(result()!.paths.map(p.basename), unorderedEquals(['ep2.mkv', 'ep3.mkv']), reason: '고른 것만'); }); testWidgets('폴더를 누르면 그 자리로', (tester) async { final (_, result) = await show(tester); await tester.tap(find.text('ep_dir')); await tester.pumpAndSettle(); expect(result()!.kind, 'reveal'); expect(p.basename(result()!.paths.single), 'ep_dir'); }); }); }