import 'dart:io'; import 'package:flutter_test/flutter_test.dart'; import 'package:jj_mkvmaker/core/explorer/dir_loader.dart'; import 'package:jj_mkvmaker/core/explorer/pane_state.dart'; import 'package:jj_mkvmaker/core/explorer/tree_rows.dart'; import 'package:jj_mkvmaker/core/file_ops.dart'; import 'package:path/path.dart' as p; /// E5a 약속 시험: lib/core/explorer/pane_state.dart · dir_loader.dart · tree_rows.dart 맨 위 약속. /// 실제 시험 폴더를 읽는다 (가짜 파일 시스템 없음). void main() { late Directory tmp; late String root, show, movies, ep1, notes; setUp(() { tmp = Directory.systemTemp.createTempSync('jj_rows_'); root = tmp.path; show = p.join(root, 'Show'); movies = p.join(root, 'Movies'); Directory(p.join(show, 'Extras')).createSync(recursive: true); Directory(movies).createSync(); ep1 = p.join(show, 'ep01.mkv'); File(ep1).writeAsStringSync('1'); File(p.join(show, 'ep02.mkv')).writeAsStringSync('22'); notes = p.join(root, 'notes.txt'); File(notes).writeAsStringSync('n'); }); tearDown(() => tmp.deleteSync(recursive: true)); List byName(List l, String _) => sortEntries(l, SortBy.name); Future open(PaneState pane, String dir) async { pane.expanded.add(dir); await loadDir(pane, dir, sort: byName); } List names(List rows) => [for (final r in rows) r.kind == TreeRow.hiddenToggle ? 'hidden' : '${' ' * r.depth}${p.basename(r.entry.path)}']; test('창 상태: 지금 폴더를 바꾸면 내역에 · 닫은 뒤 알리지 않음', () { final pane = PaneState(root); var n = 0; pane.addListener(() => n++); pane.current = show; pane.current = movies; expect(pane.nav.back, [root, show]); pane.changed(); expect(n, 1); pane.dispose(); pane.changed(); expect(n, 1); }); test('폴더 읽기: 읽음 → 정렬된 목록 · 두 번째는 읽어 둔 것 · 새로 읽기 · 읽는 동안 loading', () async { final pane = PaneState(root); final seen = []; pane.addListener(() => seen.add(pane.loading.contains(root))); expect((await loadDir(pane, root, sort: byName)).kind, 'loaded'); expect(seen.first, isTrue, reason: '읽는 동안 loading'); expect(pane.loading, isEmpty); expect([for (final e in pane.cache[root]!) p.basename(e.path)], ['Movies', 'Show', 'notes.txt']); expect((await loadDir(pane, root, sort: byName)).kind, 'cached'); File(p.join(root, 'new.txt')).writeAsStringSync('x'); var fresh = ''; expect((await loadDir(pane, root, sort: byName, force: true, onFresh: (d) => fresh = d)).kind, 'loaded'); expect(fresh, root); expect(pane.cache[root]!.length, 4); }); test('폴더 읽기 오류: 이유를 남기고 빈 목록 · 보던 폴더가 지워지면 남은 상위 폴더 · OS 가 막은 폴더 · 압축 안', () async { final pane = PaneState(root); final gone = p.join(show, 'Extras'); pane.current = gone; Directory(gone).deleteSync(); final r = await loadDir(pane, gone, sort: byName); expect([r.kind, r.vanishedUpTo], ['vanished', show]); expect(pane.cache.containsKey(gone), isFalse); final other = p.join(root, 'nope'); expect((await loadDir(pane, other, sort: byName)).kind, 'failed'); expect(pane.cache[other], isEmpty); expect(pane.errors[other], isNotEmpty, reason: '빈 목록으로 숨기지 않음'); expect((await loadDir(pane, movies, sort: byName, osBlocked: (_) => 'BLOCKED')).kind, 'loaded'); expect(pane.errors[movies], 'BLOCKED', reason: '비어 보이는 막힌 폴더'); final zip = p.join(root, 'a.zip'); await loadDir(pane, zip, sort: byName, inside: (d, f) async => [FileEntry(p.join(zip, 'in.txt'), isDir: false, modified: DateTime(0))]); expect(pane.cache[zip]!.single.path, endsWith('in.txt')); }); test('줄 목록: 펼친 것만 · 깊이 · 빠른 필터 (안에 맞는 것이 있으면 폴더는 남김) · 숨김 · 폴더만', () async { final pane = PaneState(root); await open(pane, root); await open(pane, show); List rows({bool showHidden = false, bool foldersOnly = false}) => buildTreeRows(pane, foldersOnly: foldersOnly, isHidden: (x) => samePath(x, notes), showHidden: showHidden, browsable: (_) => false); expect(names(rows()), [p.basename(root), ' Movies', ' Show', ' Extras', ' ep01.mkv', ' ep02.mkv', 'hidden']); expect(rows().last.count, 1, reason: '숨긴 수'); expect(names(rows(showHidden: true)), contains(' notes.txt')); // 폴더만 보기는 파일을 먼저 건너뛰어 숨긴 파일도 세지 않는다 (숨김 줄 없음) expect(names(rows(foldersOnly: true)), [p.basename(root), ' Movies', ' Show', ' Extras']); pane.current = show; final filtered = _Filtered(pane, 'ep02'); expect(names(buildTreeRows(filtered, isHidden: (_) => false, browsable: (_) => false)), [p.basename(root), ' Movies', ' Show', ' ep02.mkv', ' notes.txt'], reason: '지금 폴더 (Show) 안만 거르고, 밖 (Movies) 은 그대로'); }); test('줄 목록: 맨 위 다른 장치 · 즐겨찾기 묶음 (펼치면 안의 것)', () async { final pane = PaneState(root); await open(pane, root); final rows = buildTreeRows(pane, roots: [r'A:\', root, r'Z:\'], favorites: [show], isHidden: (_) => false, browsable: (_) => false); expect([for (final r in rows) r.kind].where((k) => k == TreeRow.otherRoot).length, 2); expect(rows.first.entry.path, r'A:\'); expect(rows.last.kind, TreeRow.favorites); pane.favoritesOpen = true; final open2 = buildTreeRows(pane, roots: [root], favorites: [show], isHidden: (_) => false, browsable: (_) => false); expect(open2.last.kind, TreeRow.favorite); expect(quickFilterMatch('Show.S01E02.mkv', '*.mkv'), isTrue); expect(globToRegExp('*.srt; ep??.mkv').hasMatch('EP01.MKV'), isTrue); }); } /// 빠른 필터 글을 가진 창 (화면이 칸의 글로 덮어쓰는 것과 같게) class _Filtered extends PaneState { _Filtered(PaneState from, this._f) : super(from.root) { expanded.addAll(from.expanded); cache.addAll(from.cache); current = from.current; } final String _f; @override String get filter => _f; }