import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:jj_mkvmaker/core/vault_store.dart'; import 'package:jj_mkvmaker/core/vault_view.dart'; import 'package:jj_mkvmaker/ui/vault/vault_session.dart'; import 'package:jj_mkvmaker/ui/vault/vault_views.dart'; import 'package:path/path.dart' as p; /// 보관함 나누기 (vault_page 1,095줄 → lib/ui/vault/) 의 모듈 약속 시험. 실제 시험 폴더 · 실제 보관함 (PBKDF2 1000번) 만 쓴다. /// 화면 전체 흐름 (만들기 · 열기 · 넣기 · 보기 · 꺼내기 · 폴더 · 열쇠) 은 vault_page_test 가 본다 void main() { TestWidgetsFlutterBinding.ensureInitialized(); late Directory tmp; final wasView = vaultViewRootOf; setUp(() { tmp = Directory.systemTemp.createTempSync('jj_vaultmod_'); vaultViewRootOf = () async => Directory(p.join(tmp.path, 'view')); }); tearDown(() async { vaultViewRootOf = wasView; await removeTempDirRetry(tmp); }); test('vault_session (상태): 폴더 글 · 위 폴더 · 잇기 · 상태를 바꾸면 다시 그림', () { expect(vaultParentOf('가/나/다'), '가/나'); expect(vaultParentOf('가'), ''); expect(vaultJoin('', '가'), '가'); expect(vaultJoin('가', '나'), '가/나'); expect(vaultFolderLabel('가/나'), contains('가 › 나')); var n = 0; final said = []; final s = VaultUiState(changed: () => n++, snack: said.add); s.setBusy(true); s.setProgress('1 / 2'); expect((s.busy, s.progress, n), (true, '1 / 2', 2)); s.setBusy(false); expect(s.progress, '1 / 2', reason: 'setBusy 는 진행 글을 그대로 둔다'); s.setBusyProgress(false, null); expect((s.busy, s.progress, n), (false, null, 4)); }); test('vault_session (보기 폴더): 한 번 연 동안 같은 폴더 · 지우면 잊고 다음은 새 폴더 · 없는 폴더 지우기는 조용히', () async { final v = VaultViewSession(); await v.clear(); // 만든 적 없음 final a = await v.dir(), b = await v.dir(); expect(a.path, b.path); expect(p.basename(a.path), startsWith(vaultViewPrefix)); expect(p.dirname(a.path), p.join(tmp.path, 'view')); File(p.join(a.path, 'x.bin')).writeAsBytesSync([1, 2, 3]); await v.clear(); expect(a.existsSync(), isFalse); final c = await v.dir(); expect(c.path == a.path, isFalse); await removeTempDirRetry(Directory(p.join(tmp.path, '없음'))); }); test('vault_session (넣기): 펼치기 (폴더 구조 · 겹치는 이름 " (2)" · 읽지 못한 것은 빼고) → 넣기 → 잠그면 멈춤', () async { final vault = await Vault.create(Directory(p.join(tmp.path, 'vault')), 'pw-for-test', iterations: 1000); final src = Directory(p.join(tmp.path, 'src'))..createSync(); final one = File(p.join(src.path, 'a.txt'))..writeAsStringSync('하나'); final dir = Directory(p.join(src.path, '사진'))..createSync(); File(p.join(dir.path, 'b.txt')).writeAsStringSync('둘'); Directory(p.join(dir.path, '안')).createSync(); File(p.join(dir.path, '안', 'c.txt')).writeAsStringSync('셋'); Directory(p.join(dir.path, '빈')).createSync(); await vault.mkdir('사진'); // 맨 위에 같은 이름이 이미 있음 final missing = p.join(src.path, '없는 폴더'); Directory(missing).createSync(); Directory(missing).deleteSync(); final plan = await planVaultAdd(vault, '', [one.path, dir.path, missing]); expect(plan.folders.first, '사진 (2)'); expect(plan.folders, unorderedEquals(['사진 (2)', '사진 (2)/안', '사진 (2)/빈']), reason: '안의 순서는 폴더 목록 순서'); // 없는 것은 폴더가 아니라 파일로 보여 펼치기에서는 실패하지 않는다 - 넣기에서 실패해 bad 에 남는다 expect([for (final x in plan.files) '${x.folder}|${p.basename(x.src)}'], unorderedEquals(['|a.txt', '사진 (2)|b.txt', '사진 (2)/안|c.txt', '|없는 폴더'])); final said = []; final s = VaultUiState(changed: () {}, snack: said.add)..vault = vault; final thumbs = Directory(p.join(tmp.path, 'thumbs'))..createSync(); final r = await putVaultAdd(s, [one.path, dir.path, missing], plan, thumbTemp: () async => thumbs.path); expect(r.done, 3); expect(r.bad, {missing}, reason: '넣지 못한 것만 (그 원본은 화면이 지우지 않음)'); expect(said, hasLength(1), reason: '처음 실패만 알림'); expect((s.busy, s.progress), (false, null)); expect([for (final e in vault.filesUnder('사진 (2)')) e.path], unorderedEquals(['사진 (2)/b.txt', '사진 (2)/안/c.txt'])); expect(vault.folders, contains('사진 (2)/빈')); expect(one.existsSync(), isTrue, reason: '넣기는 원본을 지우지 않는다 (지우기는 화면이 deleteOriginals 로)'); // 보관함이 잠겨 있으면 (vault = null) 하나도 넣지 않고 멈춘다 final p3 = VaultAddPlan()..files.add((src: one.path, folder: '', top: one.path)); final s3 = VaultUiState(changed: () {}, snack: said.add)..vault = null; final r3 = await putVaultAdd(s3, [one.path], p3, thumbTemp: () async => thumbs.path); expect(r3.done, 0); }); testWidgets('vault_views (묻는 창): 옮길 곳은 자기와 그 안을 빼고 · 지금 있는 곳은 꺼짐 · 이름 창의 빈 칸은 null', (t) async { String? picked = 'x', named = 'x'; late BuildContext ctx; await t.pumpWidget(MaterialApp(home: Builder(builder: (c) { ctx = c; return const Scaffold(); }))); final f = pickVaultTarget(ctx, ['가', '가/나', '다'], except: '가', from: ''); await t.pumpAndSettle(); expect(find.text('나'), findsNothing); expect(find.text('가'), findsNothing); expect(t.widget(find.widgetWithText(ListTile, '보관함 맨 위')).enabled, isFalse); await t.tap(find.text('다')); await t.pumpAndSettle(); picked = await f; expect(picked, '다'); final n = askVaultName(ctx, '새 폴더'); await t.pumpAndSettle(); await t.tap(find.text('확인')); await t.pumpAndSettle(); named = await n; expect(named, isNull); }); testWidgets('vault_views (목록): 폴더 먼저 (이름 순) · 파일은 정렬 따라 · 바쁘면 누르기 꺼짐 · 누른 것은 콜백으로', (t) async { final vault = await t.runAsync(() async { final v = await Vault.create(Directory(p.join(tmp.path, 'vault')), 'pw-for-test', iterations: 1000); final src = Directory(p.join(tmp.path, 'src'))..createSync(); for (final n in ['A.txt', 'b.txt']) { File(p.join(src.path, n)).writeAsStringSync(n); await v.add(p.join(src.path, n), name: n, folder: ''); await Future.delayed(const Duration(milliseconds: 20)); } await v.mkdir('나'); await v.mkdir('가'); return v; }); final got = []; Future show(String sort, {bool busy = false}) => t.pumpWidget(MaterialApp( home: Scaffold( body: VaultListView( vault: vault!, cwd: '', sort: sort, busy: busy, onCwd: (f) => got.add('cwd $f'), onSort: (x) => got.add('sort $x'), onFolder: (f, w) => got.add('$w $f'), onEntry: (e, w) => got.add('$w ${e.name}'))))); List titles() => [ for (final w in t.widgetList(find.byType(ListTile))) ((w.title as Text).data ?? ''), ]; await show('name'); expect(titles(), ['가', '나', 'A.txt', 'b.txt']); await show('added'); expect(titles(), ['가', '나', 'b.txt', 'A.txt'], reason: '넣은 날 새것 먼저 (b 를 나중에 넣음)'); await t.tap(find.text('가')); await t.tap(find.text('b.txt')); expect(got, ['cwd 가', 'view b.txt']); await show('added', busy: true); await t.tap(find.text('나')); expect(got, hasLength(2), reason: '바쁜 동안은 누르기가 꺼짐'); }); }