Some checks failed
CI / Windows build (push) Has been cancelled
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>
82 lines
2.5 KiB
Dart
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);
|
|
});
|
|
});
|
|
}
|