Files
BadNote/test/search_snippet_test.dart
Akiba So 3e0b81be92
Some checks failed
CI / Windows build (push) Has been cancelled
feat(f8): search snippet extraction pure core
For library-wide full-text search (the user's #1 named differentiator):
snippetFor() finds the first case-insensitive match of a query in a source
string (PDF text page / typed box / OCR'd handwriting) and returns a windowed
excerpt centered on it, preserving the match offset + length and
truncatedStart/End flags so the results list can render "…ctx **match** ctx…"
and jump to the hit. The full match is always included; near-edge matches don't
over-truncate. Returns null for empty/absent query.

The FTS index + ranking live in the DB (search_indexer, later); this is the
pure, storage-free excerpt math, fully unit-tested.

flutter analyze lib/editor clean; 164/164 tests (+8).

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

66 lines
2.2 KiB
Dart

// Tests for pure search-snippet extraction (F8).
import 'package:flutter_test/flutter_test.dart';
import 'package:badnote/editor/search/search_snippet.dart';
void main() {
test('returns null for empty query, empty source, or no match', () {
expect(snippetFor('hello world', ''), isNull);
expect(snippetFor('', 'x'), isNull);
expect(snippetFor('hello world', 'zzz'), isNull);
});
test('case-insensitive match, offset preserved within the snippet', () {
final s = snippetFor('The Quick Brown Fox', 'quick', window: 80)!;
expect(s.match.toLowerCase(), 'quick');
expect(s.text.substring(s.matchStart, s.matchStart + s.matchLength),
'Quick');
});
test('short source is returned whole, no truncation', () {
final s = snippetFor('alpha beta gamma', 'beta', window: 80)!;
expect(s.text, 'alpha beta gamma');
expect(s.truncatedStart, isFalse);
expect(s.truncatedEnd, isFalse);
expect(s.matchStart, 'alpha '.length);
});
test('long source is windowed and truncated on both sides', () {
final source = '${'a' * 200} needle ${'b' * 200}';
final s = snippetFor(source, 'needle', window: 40)!;
expect(s.truncatedStart, isTrue);
expect(s.truncatedEnd, isTrue);
expect(s.match, 'needle');
// Window is ~40 + match; nowhere near the full 400+ source.
expect(s.text.length, lessThan(80));
});
test('match near the start has no leading truncation', () {
final source = 'needle ${'b' * 200}';
final s = snippetFor(source, 'needle', window: 40)!;
expect(s.truncatedStart, isFalse);
expect(s.matchStart, 0);
expect(s.truncatedEnd, isTrue);
});
test('match near the end has no trailing truncation', () {
final source = '${'a' * 200} needle';
final s = snippetFor(source, 'needle', window: 40)!;
expect(s.truncatedEnd, isFalse);
expect(s.truncatedStart, isTrue);
});
test('the full match is included even when longer than the window', () {
final long = 'x' * 100;
final s = snippetFor('pre ${long} post', long, window: 10)!;
expect(s.match, long);
expect(s.matchLength, 100);
});
test('finds the FIRST occurrence', () {
final s = snippetFor('cat dog cat', 'cat', window: 80)!;
expect(s.matchStart, 0);
});
}