Files
BadNote/test/vault_service_test.dart
Akiba So 2f0fda5f95
All checks were successful
CI / Windows build (push) Successful in 14m14s
feat(import): one Import-file entry + vault notebooks
Phase 3. Import becomes a single top-level action beside "Create
notebook" and the library is vault-backed.

- VaultService.createNotebook copies a picked file into a fresh
  (de-duplicated) notebook folder under the vault; its sidecar lives
  beside it, so annotations travel with the file.
- Home screen: one "Import file" action with a multi-extension picker
  (pdf / docx / pptx); routes to the editor by extension.
- The document list is now a vault scan (folders with a source file),
  not the SQLite documents table — no cache, always correct.
- PPTX soffice detection fix; DOCX convert-on-import is best-effort
  and fails gracefully when LibreOffice is unavailable.

analyze clean, tests green.
2026-06-24 21:12:38 +08:00

217 lines
7.5 KiB
Dart

// Proves the Phase-0 vault root recording: the path persists through
// SharedPreferences, vaultRootValid() is true ONLY when a path is set AND its
// directory exists, and clearVaultRoot() forgets it. Uses a real temp dir for
// existence checks and a mock SharedPreferences for persistence.
import 'dart:io';
import 'package:flutter_test/flutter_test.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:badnote/services/vault_service.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
late Directory tempDir;
setUp(() async {
SharedPreferences.setMockInitialValues({});
tempDir = await Directory.systemTemp.createTemp('vault_service_test');
});
tearDown(() async {
if (await tempDir.exists()) {
await tempDir.delete(recursive: true);
}
});
Future<VaultService> makeService() async {
final prefs = await SharedPreferences.getInstance();
return VaultService.forTest(prefs);
}
test('starts with no vault root and is invalid', () async {
final vault = await makeService();
expect(vault.vaultRoot, isNull);
expect(await vault.vaultRootValid(), isFalse);
});
test('setVaultRoot persists the path', () async {
final vault = await makeService();
await vault.setVaultRoot(tempDir.path);
expect(vault.vaultRoot, tempDir.path);
// Re-read through a fresh service over the SAME prefs store: persisted.
final prefs = await SharedPreferences.getInstance();
expect(prefs.getString(VaultService.vaultRootKey), tempDir.path);
final reopened = VaultService.forTest(prefs);
expect(reopened.vaultRoot, tempDir.path);
});
test('vaultRootValid is true only when set AND the dir exists', () async {
final vault = await makeService();
// Set to an existing dir → valid.
await vault.setVaultRoot(tempDir.path);
expect(await vault.vaultRootValid(), isTrue);
// Point at a path that does not exist → invalid (folder went missing).
final missing = '${tempDir.path}${Platform.pathSeparator}gone';
await vault.setVaultRoot(missing);
expect(vault.vaultRoot, missing);
expect(await vault.vaultRootValid(), isFalse);
});
test('clearVaultRoot forgets the path and becomes invalid', () async {
final vault = await makeService();
await vault.setVaultRoot(tempDir.path);
expect(await vault.vaultRootValid(), isTrue);
await vault.clearVaultRoot();
expect(vault.vaultRoot, isNull);
expect(await vault.vaultRootValid(), isFalse);
});
test('empty stored path is treated as invalid', () async {
final vault = await makeService();
await vault.setVaultRoot('');
expect(await vault.vaultRootValid(), isFalse);
});
group('createNotebook', () {
late Directory srcDir;
setUp(() async {
srcDir = await Directory.systemTemp.createTemp('vault_src');
});
tearDown(() async {
if (await srcDir.exists()) await srcDir.delete(recursive: true);
});
Future<String> makeSource(String name, [String content = 'pdf-bytes']) async {
final f = File('${srcDir.path}${Platform.pathSeparator}$name');
await f.writeAsString(content);
return f.path;
}
test('creates folder, copies file, returns the in-vault path', () async {
final vault = await makeService();
await vault.setVaultRoot(tempDir.path);
final src = await makeSource('Calculus Lecture 3.pdf', 'hello');
final vaultPath = await vault.createNotebook(src);
// Returned path is inside the vault, in a folder named from the file.
final expectedFolder =
'${tempDir.path}${Platform.pathSeparator}Calculus Lecture 3';
expect(Directory(expectedFolder).existsSync(), isTrue);
expect(
vaultPath,
'$expectedFolder${Platform.pathSeparator}Calculus Lecture 3.pdf',
);
// The file was copied with its contents intact.
expect(File(vaultPath).existsSync(), isTrue);
expect(File(vaultPath).readAsStringSync(), 'hello');
// The original is untouched (copy, not move).
expect(File(src).existsSync(), isTrue);
});
test('de-duplicates the folder name on collision', () async {
final vault = await makeService();
await vault.setVaultRoot(tempDir.path);
final a = await makeSource('Notes.pdf', 'A');
final firstPath = await vault.createNotebook(a);
expect(
firstPath,
'${tempDir.path}${Platform.pathSeparator}Notes${Platform.pathSeparator}Notes.pdf',
);
// Re-import a file with the same basename → suffixed folder.
final b = await makeSource('Notes.pdf', 'B');
final secondPath = await vault.createNotebook(b);
expect(
secondPath,
'${tempDir.path}${Platform.pathSeparator}Notes 2${Platform.pathSeparator}Notes.pdf',
);
expect(File(secondPath).readAsStringSync(), 'B');
// First copy still intact.
expect(File(firstPath).readAsStringSync(), 'A');
});
test('throws when no vault root is set', () async {
final vault = await makeService();
final src = await makeSource('x.pdf');
expect(() => vault.createNotebook(src), throwsStateError);
});
});
group('scanNotebooks', () {
Future<String> writeFile(String path, [String content = 'x']) async {
final f = File(path);
await f.create(recursive: true);
await f.writeAsString(content);
return f.path;
}
test('empty / missing vault yields an empty list', () async {
final vault = await makeService();
// No root set.
expect(await vault.scanNotebooks(), isEmpty);
// Root set but empty.
await vault.setVaultRoot(tempDir.path);
expect(await vault.scanNotebooks(), isEmpty);
});
test('lists notebook folders that contain a source file', () async {
final vault = await makeService();
await vault.setVaultRoot(tempDir.path);
final sep = Platform.pathSeparator;
// A PDF notebook with a sidecar.
await writeFile('${tempDir.path}${sep}Lecture${sep}Lecture.pdf');
await writeFile(
'${tempDir.path}${sep}Lecture${sep}Lecture.pdf.badnote.json',
'{}',
);
// A PPTX notebook without a sidecar.
await writeFile('${tempDir.path}${sep}Deck${sep}Deck.pptx');
// A hidden vault-metadata folder is skipped.
await writeFile('${tempDir.path}$sep.badnote${sep}vault.json', '{}');
// A folder with no importable file is skipped.
await writeFile('${tempDir.path}${sep}Empty${sep}readme.txt');
final notebooks = await vault.scanNotebooks();
final byName = {for (final n in notebooks) n.filename: n};
expect(byName.keys, containsAll(['Lecture.pdf', 'Deck.pptx']));
expect(notebooks.length, 2);
expect(byName['Lecture.pdf']!.docType, 'pdf');
expect(byName['Lecture.pdf']!.hasSidecar, isTrue);
expect(byName['Deck.pptx']!.docType, 'pptx');
expect(byName['Deck.pptx']!.hasSidecar, isFalse);
});
test('a created notebook is discoverable by the scan', () async {
final vault = await makeService();
await vault.setVaultRoot(tempDir.path);
final srcDir = await Directory.systemTemp.createTemp('vault_scan_src');
try {
final src = '${srcDir.path}${Platform.pathSeparator}Doc.pdf';
await File(src).writeAsString('bytes');
await vault.createNotebook(src);
final notebooks = await vault.scanNotebooks();
expect(notebooks.length, 1);
expect(notebooks.first.filename, 'Doc.pdf');
expect(notebooks.first.docType, 'pdf');
} finally {
await srcDir.delete(recursive: true);
}
});
});
}