Files
BadNote/test/link_graph_test.dart
Akiba So 852eb389ee
Some checks failed
CI / Windows build (push) Has been cancelled
feat: 双链 link graph pure core — [[link]] parse + backlinks (F7)
Pure bidirectional-link engine for the sticky-note/board system (a user-named
differentiator). parseLinkTargets extracts trimmed, de-duped [[targets]];
LinkGraph builds forward + backlink indices (fromTexts parses, fromLinks takes
explicit targets), ignores self-links, and danglingTargets() surfaces links to
unknown nodes. Widget-free + storage-free so it is fully unit-tested; the board
UI + persistence wrap it later.

Built ahead of its phase deliberately as a zero-rework-risk pure data structure
(not rendering/perf — the P0.5 device gate can't invalidate it).

flutter analyze lib/editor clean; 139/139 tests (+10: parsing, backlinks,
self-link, dangling, immutability).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 03:19:08 +08:00

82 lines
2.5 KiB
Dart

// Tests for the 双链 (bidirectional link) pure core (F7): [[link]] parsing +
// forward/backlink indices + dangling-link detection.
import 'package:flutter_test/flutter_test.dart';
import 'package:badnote/editor/link/link_graph.dart';
void main() {
group('parseLinkTargets', () {
test('extracts bracketed targets, trimmed', () {
expect(parseLinkTargets('see [[Alpha]] and [[ Beta ]]'),
['Alpha', 'Beta']);
});
test('de-duplicates, keeping first-occurrence order', () {
expect(parseLinkTargets('[[A]] [[B]] [[A]]'), ['A', 'B']);
});
test('ignores empty / whitespace-only targets', () {
expect(parseLinkTargets('x [[]] y [[ ]] z [[Real]]'), ['Real']);
});
test('no links → empty', () {
expect(parseLinkTargets('plain text, no links'), isEmpty);
});
});
group('LinkGraph', () {
test('backlinksOf collects every node linking to a target', () {
final g = LinkGraph.fromTexts({
'n1': 'links to [[n2]] and [[n3]]',
'n2': 'links to [[n3]]',
'n3': 'a leaf',
});
expect(g.linksFrom('n1'), {'n2', 'n3'});
expect(g.linksFrom('n3'), isEmpty);
expect(g.backlinksOf('n3'), {'n1', 'n2'});
expect(g.backlinksOf('n2'), {'n1'});
expect(g.backlinksOf('n1'), isEmpty);
});
test('self-links are ignored', () {
final g = LinkGraph.fromTexts({'n1': 'I reference [[n1]] myself'});
expect(g.linksFrom('n1'), isEmpty);
expect(g.backlinksOf('n1'), isEmpty);
});
test('fromLinks builds the same indices from explicit targets', () {
final g = LinkGraph.fromLinks({
'a': ['b', 'c'],
'b': ['c'],
'c': [],
});
expect(g.backlinksOf('c'), {'a', 'b'});
expect(g.linksFrom('a'), {'b', 'c'});
});
test('danglingTargets surfaces links to unknown nodes', () {
final g = LinkGraph.fromLinks({
'a': ['b', 'ghost'],
'b': [],
});
expect(g.danglingTargets(), {'ghost'});
});
test('returned sets are unmodifiable', () {
final g = LinkGraph.fromLinks({
'a': ['b'],
'b': [],
});
expect(() => g.linksFrom('a').add('x'), throwsUnsupportedError);
expect(() => g.backlinksOf('b').add('x'), throwsUnsupportedError);
});
test('unknown node ids yield empty (no crash)', () {
final g = LinkGraph.fromLinks({'a': []});
expect(g.linksFrom('nope'), isEmpty);
expect(g.backlinksOf('nope'), isEmpty);
});
});
}