JJ_MKVMaker/test/explorer_rows_contract_test...

133 lines
6.2 KiB
Dart

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<FileEntry> byName(List<FileEntry> l, String _) => sortEntries(l, SortBy.name);
Future<void> open(PaneState pane, String dir) async {
pane.expanded.add(dir);
await loadDir(pane, dir, sort: byName);
}
List<String> names(List<TreeRow> 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 = <bool>[];
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<TreeRow> 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;
}