refactor(tests): group test files by feature, drop phase/cluster naming
Restructures the integration test layout so each file is named after
what it covers, not the implementation phase it landed in.
nuwiki-core (renames only):
vimwiki_lexer → lexer
vimwiki_parser → parser
vimwiki_tags → tags
vimwiki_table_alignment → table_alignment
diary_period → diary
list_continuation → lists
transclusion_attrs → transclusion
+ table colspan/rowspan tests moved here from parity_cluster_1
nuwiki-lsp (renames + merges + one split):
cluster_a_list_rewriters → commands_lists
cluster_b_table_rewriters → commands_tables
cluster_c_link_helpers +
phase19_followlink_creates → commands_links
phase13_rename_commands → commands_files
phase17_colorize → commands_colorize
phase17_html_export → html_export
phase15_link_health → link_health
phase18_multi_wiki → multi_wiki
phase19_folding → folding
nav → navigation
lsp_helpers → helpers
phase16_diary +
diary_frequency → diary
phase11_plumbing +
parity_cluster_1 (config) → index_and_config
tags_index_and_lsp +
phase17_backfill → commands_tags
phase14_edit_commands → split into
commands_checkboxes,
commands_headings,
commands_tasks
Net: 30 → 28 integration-test files. 456 → 455 tests (the one
removed test was a dummy `paragraph_render_is_unchanged` whose only
purpose was to keep a `ParagraphNode` import alive in
parity_cluster_1; the import is exercised elsewhere now).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,228 @@
|
||||
//! Tests for the Phase 8 navigation pieces: WorkspaceIndex queries,
|
||||
//! page-name derivation, anchor slugify, position lookup, and link
|
||||
//! resolution. The async LSP handlers are exercised through these
|
||||
//! helpers; end-to-end JSON-RPC drive lives in a follow-up phase.
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use nuwiki_core::ast::{InlineNode, LinkKind};
|
||||
use nuwiki_core::syntax::vimwiki::VimwikiSyntax;
|
||||
use nuwiki_core::syntax::SyntaxPlugin;
|
||||
use nuwiki_lsp::index::{page_name_from_uri, slugify, WorkspaceIndex};
|
||||
use nuwiki_lsp::nav::{find_inline_at, lsp_to_byte_pos};
|
||||
use tower_lsp::lsp_types::{Position as LspPosition, Url};
|
||||
|
||||
fn parse(src: &str) -> nuwiki_core::ast::DocumentNode {
|
||||
VimwikiSyntax::new().parse(src)
|
||||
}
|
||||
|
||||
// ===== Slugify =====
|
||||
|
||||
#[test]
|
||||
fn slugify_basic_title() {
|
||||
assert_eq!(slugify("Hello World"), "hello-world");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn slugify_strips_punctuation_and_collapses_dashes() {
|
||||
assert_eq!(slugify("Hello, World! Again"), "hello-world-again");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn slugify_unicode_lowercase() {
|
||||
assert_eq!(slugify("Crème Brûlée"), "crème-brûlée");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn slugify_no_trailing_dash() {
|
||||
assert_eq!(slugify("Title — "), "title");
|
||||
}
|
||||
|
||||
// ===== Page name from URI =====
|
||||
|
||||
#[test]
|
||||
fn page_name_relative_to_root() {
|
||||
let root = PathBuf::from("/wiki");
|
||||
let uri = Url::from_file_path("/wiki/dir/Page.wiki").unwrap();
|
||||
assert_eq!(page_name_from_uri(&uri, Some(&root)), "dir/Page");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn page_name_at_root() {
|
||||
let root = PathBuf::from("/wiki");
|
||||
let uri = Url::from_file_path("/wiki/Index.wiki").unwrap();
|
||||
assert_eq!(page_name_from_uri(&uri, Some(&root)), "Index");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn page_name_without_root_falls_back_to_stem() {
|
||||
let uri = Url::from_file_path("/tmp/foo.wiki").unwrap();
|
||||
assert_eq!(page_name_from_uri(&uri, None), "foo");
|
||||
}
|
||||
|
||||
// ===== Workspace index =====
|
||||
|
||||
#[test]
|
||||
fn upsert_indexes_headings_and_outgoing_links() {
|
||||
let mut idx = WorkspaceIndex::new(Some(PathBuf::from("/wiki")));
|
||||
let uri = Url::from_file_path("/wiki/Home.wiki").unwrap();
|
||||
let ast = parse("= Home =\n[[Other]] and [[#Section]]\n");
|
||||
idx.upsert(uri.clone(), &ast);
|
||||
|
||||
let page = idx.page(&uri).expect("page indexed");
|
||||
assert_eq!(page.name, "Home");
|
||||
assert_eq!(page.headings.len(), 1);
|
||||
assert_eq!(page.headings[0].title, "Home");
|
||||
assert_eq!(page.headings[0].anchor, "home");
|
||||
assert_eq!(page.outgoing.len(), 2);
|
||||
assert_eq!(page.outgoing[0].target_page.as_deref(), Some("Other"));
|
||||
assert_eq!(page.outgoing[0].kind, LinkKind::Wiki);
|
||||
assert_eq!(page.outgoing[1].kind, LinkKind::AnchorOnly);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn upsert_registers_page_by_name_and_records_backlinks() {
|
||||
let mut idx = WorkspaceIndex::new(Some(PathBuf::from("/wiki")));
|
||||
let home = Url::from_file_path("/wiki/Home.wiki").unwrap();
|
||||
let about = Url::from_file_path("/wiki/About.wiki").unwrap();
|
||||
|
||||
idx.upsert(home.clone(), &parse("[[About]]\n"));
|
||||
idx.upsert(about.clone(), &parse("= About =\n"));
|
||||
|
||||
assert_eq!(idx.page_by_name("About").unwrap().uri, about);
|
||||
|
||||
let backlinks = idx.backlinks_for("About");
|
||||
assert_eq!(backlinks.len(), 1);
|
||||
assert_eq!(backlinks[0].source_uri, home);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn upsert_replaces_prior_indexing_for_same_uri() {
|
||||
let mut idx = WorkspaceIndex::new(Some(PathBuf::from("/wiki")));
|
||||
let uri = Url::from_file_path("/wiki/Home.wiki").unwrap();
|
||||
idx.upsert(uri.clone(), &parse("[[A]]\n"));
|
||||
assert_eq!(idx.backlinks_for("A").len(), 1);
|
||||
|
||||
// Re-upsert with different content: old backlinks must drop.
|
||||
idx.upsert(uri.clone(), &parse("[[B]]\n"));
|
||||
assert_eq!(idx.backlinks_for("A").len(), 0);
|
||||
assert_eq!(idx.backlinks_for("B").len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remove_drops_page_and_backlinks() {
|
||||
let mut idx = WorkspaceIndex::new(Some(PathBuf::from("/wiki")));
|
||||
let home = Url::from_file_path("/wiki/Home.wiki").unwrap();
|
||||
let about = Url::from_file_path("/wiki/About.wiki").unwrap();
|
||||
idx.upsert(home.clone(), &parse("[[About]]\n"));
|
||||
idx.upsert(about.clone(), &parse("= About =\n"));
|
||||
|
||||
idx.remove(&home);
|
||||
assert!(idx.page(&home).is_none());
|
||||
assert!(idx.backlinks_for("About").is_empty());
|
||||
// The target page is untouched.
|
||||
assert!(idx.page(&about).is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_wiki_link_to_uri() {
|
||||
let mut idx = WorkspaceIndex::new(Some(PathBuf::from("/wiki")));
|
||||
let about = Url::from_file_path("/wiki/About.wiki").unwrap();
|
||||
idx.upsert(about.clone(), &parse("= About =\n"));
|
||||
|
||||
let target = nuwiki_core::ast::LinkTarget {
|
||||
kind: LinkKind::Wiki,
|
||||
path: Some("About".into()),
|
||||
..Default::default()
|
||||
};
|
||||
assert_eq!(idx.resolve(&target, "Home"), Some(&about));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_anchor_only_link_returns_source_page() {
|
||||
let mut idx = WorkspaceIndex::new(Some(PathBuf::from("/wiki")));
|
||||
let home = Url::from_file_path("/wiki/Home.wiki").unwrap();
|
||||
idx.upsert(home.clone(), &parse("= H =\n[[#Section]]\n"));
|
||||
|
||||
let target = nuwiki_core::ast::LinkTarget {
|
||||
kind: LinkKind::AnchorOnly,
|
||||
anchor: Some("section".into()),
|
||||
..Default::default()
|
||||
};
|
||||
assert_eq!(idx.resolve(&target, "Home"), Some(&home));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn page_names_lists_indexed_pages_sorted() {
|
||||
let mut idx = WorkspaceIndex::new(Some(PathBuf::from("/wiki")));
|
||||
for name in ["Charlie", "Alpha", "Bravo"] {
|
||||
let uri = Url::from_file_path(format!("/wiki/{name}.wiki")).unwrap();
|
||||
idx.upsert(uri, &parse(&format!("= {name} =\n")));
|
||||
}
|
||||
let names = idx.page_names();
|
||||
assert_eq!(names, vec!["Alpha", "Bravo", "Charlie"]);
|
||||
}
|
||||
|
||||
// ===== Position conversion =====
|
||||
|
||||
#[test]
|
||||
fn lsp_to_byte_pos_passthrough_in_utf8() {
|
||||
let p = LspPosition {
|
||||
line: 2,
|
||||
character: 5,
|
||||
};
|
||||
let (line, col) = lsp_to_byte_pos(p, "ignored", true);
|
||||
assert_eq!((line, col), (2, 5));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lsp_to_byte_pos_converts_from_utf16() {
|
||||
// 'é' is one UTF-16 code unit, two UTF-8 bytes. Char "2" after 'é':
|
||||
// UTF-16 col 2 → byte col 3.
|
||||
let text = "héllo\n";
|
||||
let p = LspPosition {
|
||||
line: 0,
|
||||
character: 2,
|
||||
};
|
||||
let (line, col) = lsp_to_byte_pos(p, text, false);
|
||||
assert_eq!(line, 0);
|
||||
assert_eq!(col, 3);
|
||||
}
|
||||
|
||||
// ===== Position lookup =====
|
||||
|
||||
#[test]
|
||||
fn find_inline_at_returns_wikilink_under_cursor() {
|
||||
let src = "see [[Other]] now\n";
|
||||
let doc = parse(src);
|
||||
// Cursor inside "Other" — byte col 6 falls inside "[[Other]]" (which
|
||||
// starts at byte 4 and ends at byte 13).
|
||||
let node = find_inline_at(&doc, 0, 6).expect("wikilink at cursor");
|
||||
assert!(matches!(node, InlineNode::WikiLink(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_inline_at_returns_none_for_plain_text() {
|
||||
let src = "just text\n";
|
||||
let doc = parse(src);
|
||||
let node = find_inline_at(&doc, 0, 3);
|
||||
// "just text" → Text node, which is what we get back from the inner
|
||||
// walk (not None). Verify it's a Text.
|
||||
assert!(matches!(node, Some(InlineNode::Text(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_inline_at_descends_into_inline_containers() {
|
||||
let src = "*bold text*\n";
|
||||
let doc = parse(src);
|
||||
// Inside "bold" — should return the inner Text, not the wrapping Bold.
|
||||
let node = find_inline_at(&doc, 0, 3).expect("hit");
|
||||
assert!(matches!(node, InlineNode::Text(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_inline_at_returns_none_past_eol() {
|
||||
let src = "short\n";
|
||||
let doc = parse(src);
|
||||
assert!(find_inline_at(&doc, 0, 999).is_none());
|
||||
}
|
||||
Reference in New Issue
Block a user