feat(storage): notes are vault sidecar notebooks
All checks were successful
CI / Windows build (push) Successful in 12m55s
All checks were successful
CI / Windows build (push) Successful in 12m55s
Phase 4. Standalone notes move off SQLite into the vault, like the PDF annotations. - "Create notebook" makes a vault folder with a notebook.badnote.json (BadnoteSidecar docType 'notebook' + a title field), opened via SidecarRepository. - PenNoteScreen loads/saves its strokes (page 0) + title to that sidecar instead of the SQLite Note model. - note_provider lists notes from a vault scan (VaultService.scanNotes = folders with notebook.badnote.json and no source file); the doc scan still excludes them. Delete removes the folder. PDF/slide editors unchanged; pre-existing SQLite notes migrate in Phase 5. analyze clean, tests green.
This commit is contained in:
@@ -9,6 +9,7 @@ import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import 'package:badnote/services/vault_service.dart';
|
||||
import 'package:badnote/storage/sidecar_store.dart';
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
@@ -213,4 +214,129 @@ void main() {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
group('createEmptyNotebook + scanNotes (Phase 4 standalone notebooks)', () {
|
||||
test('writes notebook.badnote.json with the title and docType notebook',
|
||||
() async {
|
||||
final vault = await makeService();
|
||||
await vault.setVaultRoot(tempDir.path);
|
||||
final sep = Platform.pathSeparator;
|
||||
|
||||
final notePath = await vault.createEmptyNotebook('My Algebra Notes');
|
||||
|
||||
// Synthetic note path is <folder>/notebook, folder named from the title.
|
||||
final expectedFolder = '${tempDir.path}${sep}My Algebra Notes';
|
||||
expect(notePath, '$expectedFolder${sep}notebook');
|
||||
|
||||
// The sidecar exists at <folder>/notebook.badnote.json and carries the
|
||||
// title + docType, but NO fake source file was created.
|
||||
final sidecarFile = File('$notePath.badnote.json');
|
||||
expect(sidecarFile.existsSync(), isTrue);
|
||||
final loaded =
|
||||
await SidecarStore.read(sidecarFile);
|
||||
expect(loaded, isNotNull);
|
||||
expect(loaded!.title, 'My Algebra Notes');
|
||||
expect(loaded.docType, 'notebook');
|
||||
// The folder holds only the sidecar (and possibly its .bak/.tmp), never
|
||||
// an importable source file.
|
||||
final files = Directory(expectedFolder)
|
||||
.listSync()
|
||||
.whereType<File>()
|
||||
.map((f) => f.uri.pathSegments.last)
|
||||
.toList();
|
||||
expect(files, contains('notebook.badnote.json'));
|
||||
expect(
|
||||
files.any((n) =>
|
||||
n.endsWith('.pdf') ||
|
||||
n.endsWith('.pptx') ||
|
||||
n.endsWith('.docx') ||
|
||||
n.endsWith('.ppt')),
|
||||
isFalse,
|
||||
);
|
||||
});
|
||||
|
||||
test('de-duplicates the notebook folder name on title collision', () async {
|
||||
final vault = await makeService();
|
||||
await vault.setVaultRoot(tempDir.path);
|
||||
final sep = Platform.pathSeparator;
|
||||
|
||||
final a = await vault.createEmptyNotebook('Journal');
|
||||
final b = await vault.createEmptyNotebook('Journal');
|
||||
expect(a, '${tempDir.path}${sep}Journal${sep}notebook');
|
||||
expect(b, '${tempDir.path}${sep}Journal 2${sep}notebook');
|
||||
});
|
||||
|
||||
test('blank title falls back to an Untitled folder, null sidecar title',
|
||||
() async {
|
||||
final vault = await makeService();
|
||||
await vault.setVaultRoot(tempDir.path);
|
||||
final sep = Platform.pathSeparator;
|
||||
|
||||
final notePath = await vault.createEmptyNotebook(' ');
|
||||
expect(notePath, '${tempDir.path}${sep}Untitled${sep}notebook');
|
||||
final loaded = await SidecarStore.read(File('$notePath.badnote.json'));
|
||||
expect(loaded!.title, isNull);
|
||||
});
|
||||
|
||||
test('scanNotes lists note folders; scanNotebooks excludes them', () async {
|
||||
final vault = await makeService();
|
||||
await vault.setVaultRoot(tempDir.path);
|
||||
final sep = Platform.pathSeparator;
|
||||
|
||||
// A standalone note.
|
||||
final notePath = await vault.createEmptyNotebook('Ideas');
|
||||
// A file-backed document notebook.
|
||||
final docFolder = '${tempDir.path}${sep}Lecture';
|
||||
final doc = File('$docFolder${sep}Lecture.pdf');
|
||||
await doc.create(recursive: true);
|
||||
await doc.writeAsString('pdf');
|
||||
|
||||
final notes = await vault.scanNotes();
|
||||
expect(notes.length, 1, reason: 'only the standalone note is a note');
|
||||
expect(notes.first.title, 'Ideas');
|
||||
expect(notes.first.notePath, notePath);
|
||||
expect(notes.first.folderPath, '${tempDir.path}${sep}Ideas');
|
||||
|
||||
// The document notebook is NOT a note...
|
||||
expect(notes.any((n) => n.folderPath == docFolder), isFalse);
|
||||
// ...and the standalone note is NOT a document.
|
||||
final notebooks = await vault.scanNotebooks();
|
||||
expect(notebooks.length, 1);
|
||||
expect(notebooks.first.filename, 'Lecture.pdf');
|
||||
expect(
|
||||
notebooks.any((nb) => nb.folderPath == '${tempDir.path}${sep}Ideas'),
|
||||
isFalse,
|
||||
);
|
||||
});
|
||||
|
||||
test('note title round-trips through the sidecar (title persists)',
|
||||
() async {
|
||||
final vault = await makeService();
|
||||
await vault.setVaultRoot(tempDir.path);
|
||||
|
||||
final notePath = await vault.createEmptyNotebook('Round Trip');
|
||||
final notes = await vault.scanNotes();
|
||||
expect(notes.single.title, 'Round Trip');
|
||||
|
||||
// Re-open the sidecar directly: the title is still there.
|
||||
final loaded = await SidecarStore.read(File('$notePath.badnote.json'));
|
||||
expect(loaded!.title, 'Round Trip');
|
||||
});
|
||||
|
||||
test('a note folder whose sidecar title is missing falls back to folder',
|
||||
() async {
|
||||
final vault = await makeService();
|
||||
await vault.setVaultRoot(tempDir.path);
|
||||
final sep = Platform.pathSeparator;
|
||||
|
||||
// Hand-write a note sidecar with no title field.
|
||||
final folder = '${tempDir.path}${sep}Loose Note';
|
||||
final sidecar = File('$folder${sep}notebook.badnote.json');
|
||||
await sidecar.create(recursive: true);
|
||||
await sidecar.writeAsString('{"docType":"notebook"}');
|
||||
|
||||
final notes = await vault.scanNotes();
|
||||
expect(notes.single.title, 'Loose Note');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user