Skip to content

Commit 0546870

Browse files
committed
test: add comprehensive unit tests for FrontmatterParser, MetaJson, and NavigationGraphBuilder. Validate parsing, handling of edge cases, and document structure resolution.
1 parent 290de05 commit 0546870

4 files changed

Lines changed: 344 additions & 11 deletions

File tree

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
using ShellDocs.Core;
2+
using Xunit;
3+
4+
namespace ShellDocs.Tests;
5+
6+
public class FrontmatterParserTests
7+
{
8+
[Fact]
9+
public void Parse_ExtractsFrontmatterAndBody()
10+
{
11+
var source = "---\ntitle: Button\ndescription: A clickable thing\norder: 5\n---\n# Button\n\nBody here.";
12+
13+
var result = FrontmatterParser.Parse(source);
14+
15+
Assert.Equal("Button", result.Frontmatter.GetValue<string>("title"));
16+
Assert.Equal("A clickable thing", result.Frontmatter.GetValue<string>("description"));
17+
Assert.Equal(5, result.Frontmatter.GetValue<int>("order"));
18+
Assert.Contains("# Button", result.Body);
19+
Assert.Contains("Body here.", result.Body);
20+
}
21+
22+
[Fact]
23+
public void Parse_HandlesMissingFrontmatter()
24+
{
25+
var source = "# No Frontmatter\n\nJust markdown.";
26+
var result = FrontmatterParser.Parse(source);
27+
28+
Assert.Empty(result.Frontmatter);
29+
Assert.Equal(source, result.Body);
30+
}
31+
32+
[Fact]
33+
public void Parse_HandlesEmptyDocument()
34+
{
35+
var result = FrontmatterParser.Parse("");
36+
37+
Assert.Empty(result.Frontmatter);
38+
Assert.Equal("", result.Body);
39+
}
40+
41+
[Fact]
42+
public void Parse_HandlesUnclosedFrontmatter()
43+
{
44+
var source = "---\ntitle: Broken\n\n# Body";
45+
var result = FrontmatterParser.Parse(source);
46+
47+
Assert.Empty(result.Frontmatter);
48+
Assert.Equal(source, result.Body);
49+
}
50+
51+
[Fact]
52+
public void Parse_HandlesInvalidYaml()
53+
{
54+
var source = "---\ntitle: [unclosed\n---\n# Body";
55+
var result = FrontmatterParser.Parse(source);
56+
57+
Assert.Empty(result.Frontmatter);
58+
Assert.Contains("# Body", result.Body);
59+
}
60+
61+
[Fact]
62+
public void GetValue_CoercesTypes()
63+
{
64+
var source = "---\nage: 42\n---\n";
65+
var result = FrontmatterParser.Parse(source);
66+
67+
Assert.Equal(42, result.Frontmatter.GetValue<int>("age"));
68+
Assert.Equal("42", result.Frontmatter.GetValue<string>("age"));
69+
}
70+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
using ShellDocs.Core;
2+
using Xunit;
3+
4+
namespace ShellDocs.Tests;
5+
6+
public class MetaJsonTests
7+
{
8+
[Fact]
9+
public void Parse_HandlesFlatSlugList()
10+
{
11+
var json = """
12+
{ "title": "Docs", "pages": ["intro", "installation", "theming"] }
13+
""";
14+
15+
var meta = MetaJson.Parse(json);
16+
17+
Assert.NotNull(meta);
18+
Assert.Equal("Docs", meta!.Title);
19+
Assert.Equal(3, meta.Pages.Count);
20+
Assert.All(meta.Pages, e => Assert.IsType<MetaJsonPageRef>(e));
21+
Assert.Equal("intro", ((MetaJsonPageRef)meta.Pages[0]).Slug);
22+
}
23+
24+
[Fact]
25+
public void Parse_RecognizesDivider()
26+
{
27+
var json = """
28+
{ "pages": ["a", "---", "b"] }
29+
""";
30+
31+
var meta = MetaJson.Parse(json);
32+
33+
Assert.NotNull(meta);
34+
Assert.IsType<MetaJsonPageRef>(meta!.Pages[0]);
35+
Assert.IsType<MetaJsonDivider>(meta.Pages[1]);
36+
Assert.IsType<MetaJsonPageRef>(meta.Pages[2]);
37+
}
38+
39+
[Fact]
40+
public void Parse_HandlesSubsectionObject()
41+
{
42+
var json = """
43+
{
44+
"title": "Components",
45+
"pages": [
46+
"button",
47+
{
48+
"title": "Data Display",
49+
"pages": ["table", "card"]
50+
}
51+
]
52+
}
53+
""";
54+
55+
var meta = MetaJson.Parse(json);
56+
57+
Assert.NotNull(meta);
58+
Assert.Equal(2, meta!.Pages.Count);
59+
var sub = Assert.IsType<MetaJsonSubsection>(meta.Pages[1]);
60+
Assert.Equal("Data Display", sub.Title);
61+
Assert.Equal(2, sub.Pages.Count);
62+
Assert.All(sub.Pages, e => Assert.IsType<MetaJsonPageRef>(e));
63+
}
64+
65+
[Fact]
66+
public void Parse_ReturnsNullForBlank()
67+
{
68+
Assert.Null(MetaJson.Parse(""));
69+
Assert.Null(MetaJson.Parse(" "));
70+
}
71+
}
Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
using ShellDocs.Core;
2+
using Xunit;
3+
4+
namespace ShellDocs.Tests;
5+
6+
public class NavigationGraphBuilderTests : IDisposable
7+
{
8+
private readonly string _root;
9+
10+
public NavigationGraphBuilderTests()
11+
{
12+
_root = Path.Combine(Path.GetTempPath(), "shelldocs-test-" + Guid.NewGuid().ToString("N"));
13+
Directory.CreateDirectory(_root);
14+
}
15+
16+
public void Dispose()
17+
{
18+
try { Directory.Delete(_root, recursive: true); } catch { }
19+
}
20+
21+
private void WriteMd(string relativePath, string title, int order = 0)
22+
{
23+
var full = Path.Combine(_root, relativePath);
24+
Directory.CreateDirectory(Path.GetDirectoryName(full)!);
25+
File.WriteAllText(full, $"---\ntitle: {title}\norder: {order}\n---\n# {title}\n");
26+
}
27+
28+
private void WriteMeta(string folder, string json)
29+
{
30+
var full = Path.Combine(_root, folder, "meta.json");
31+
Directory.CreateDirectory(Path.GetDirectoryName(full)!);
32+
File.WriteAllText(full, json);
33+
}
34+
35+
[Fact]
36+
public void Build_SingleFile_ResolvesByUrl()
37+
{
38+
WriteMd("intro.md", "Introduction");
39+
40+
var graph = NavigationGraphBuilder.Build(_root);
41+
var node = graph.ResolveByUrl("/intro");
42+
43+
Assert.NotNull(node);
44+
Assert.Equal("Introduction", node!.Title);
45+
Assert.Equal(NodeKind.Page, node.Kind);
46+
}
47+
48+
[Fact]
49+
public void Build_UsesFrontmatterTitle_FallsBackToSlug()
50+
{
51+
WriteMd("with-title.md", "Explicit");
52+
var noFrontmatter = Path.Combine(_root, "no-title.md");
53+
File.WriteAllText(noFrontmatter, "# Just markdown");
54+
55+
var graph = NavigationGraphBuilder.Build(_root);
56+
57+
Assert.Equal("Explicit", graph.ResolveByUrl("/with-title")!.Title);
58+
Assert.Equal("No Title", graph.ResolveByUrl("/no-title")!.Title);
59+
}
60+
61+
[Fact]
62+
public void Build_NestedFolders_AreSections()
63+
{
64+
WriteMd("docs/introduction.md", "Introduction");
65+
WriteMd("docs/installation.md", "Installation");
66+
WriteMd("components/button.md", "Button");
67+
68+
var graph = NavigationGraphBuilder.Build(_root);
69+
70+
Assert.NotNull(graph.ResolveByUrl("/docs/introduction"));
71+
Assert.NotNull(graph.ResolveByUrl("/docs/installation"));
72+
Assert.NotNull(graph.ResolveByUrl("/components/button"));
73+
74+
var sections = graph.Root.Children.Where(c => c.Kind == NodeKind.Section).ToList();
75+
Assert.Contains(sections, s => s.Title == "Components");
76+
Assert.Contains(sections, s => s.Title == "Docs");
77+
}
78+
79+
[Fact]
80+
public void Build_UsesMetaJsonOrdering()
81+
{
82+
WriteMd("alpha.md", "Alpha");
83+
WriteMd("bravo.md", "Bravo");
84+
WriteMd("charlie.md", "Charlie");
85+
WriteMeta("", """{ "pages": ["charlie", "alpha", "bravo"] }""");
86+
87+
var graph = NavigationGraphBuilder.Build(_root);
88+
var titles = graph.Root.Children.Select(c => c.Title).ToList();
89+
90+
Assert.Equal(new[] { "Charlie", "Alpha", "Bravo" }, titles);
91+
}
92+
93+
[Fact]
94+
public void Build_MetaJsonDivider_ProducesDividerNode()
95+
{
96+
WriteMd("a.md", "A");
97+
WriteMd("b.md", "B");
98+
WriteMeta("", """{ "pages": ["a", "---", "b"] }""");
99+
100+
var graph = NavigationGraphBuilder.Build(_root);
101+
102+
var kinds = graph.Root.Children.Select(c => c.Kind).ToList();
103+
Assert.Equal(new[] { NodeKind.Page, NodeKind.Divider, NodeKind.Page }, kinds);
104+
}
105+
106+
[Fact]
107+
public void Build_MetaJsonSubsection_CreatesNamedSection()
108+
{
109+
WriteMd("table.md", "Table");
110+
WriteMd("card.md", "Card");
111+
WriteMeta("", """
112+
{
113+
"pages": [
114+
{
115+
"title": "Data Display",
116+
"pages": ["table", "card"]
117+
}
118+
]
119+
}
120+
""");
121+
122+
var graph = NavigationGraphBuilder.Build(_root);
123+
var section = Assert.Single(graph.Root.Children);
124+
125+
Assert.Equal("Data Display", section.Title);
126+
Assert.Equal(NodeKind.Section, section.Kind);
127+
Assert.Equal(new[] { "Table", "Card" }, section.Children.Select(c => c.Title));
128+
}
129+
130+
[Fact]
131+
public void Build_UnknownSlugInMetaJson_IsSilentlySkipped()
132+
{
133+
WriteMd("real.md", "Real");
134+
WriteMeta("", """{ "pages": ["real", "does-not-exist"] }""");
135+
136+
var graph = NavigationGraphBuilder.Build(_root);
137+
var titles = graph.Root.Children.Select(c => c.Title).ToList();
138+
139+
Assert.Equal(new[] { "Real" }, titles);
140+
}
141+
142+
[Fact]
143+
public void Build_ThrowsOnMissingContentRoot()
144+
{
145+
Assert.Throws<DirectoryNotFoundException>(
146+
() => NavigationGraphBuilder.Build(Path.Combine(_root, "does-not-exist")));
147+
}
148+
149+
[Fact]
150+
public void GetPrevNext_ReturnsAdjacentPages()
151+
{
152+
WriteMd("first.md", "First", order: 1);
153+
WriteMd("second.md", "Second", order: 2);
154+
WriteMd("third.md", "Third", order: 3);
155+
156+
var graph = NavigationGraphBuilder.Build(_root);
157+
var second = graph.ResolveByUrl("/second")!;
158+
159+
var (prev, next) = graph.GetPrevNext(second);
160+
161+
Assert.Equal("First", prev?.Title);
162+
Assert.Equal("Third", next?.Title);
163+
}
164+
165+
[Fact]
166+
public void GetPrevNext_HandlesBoundaries()
167+
{
168+
WriteMd("only.md", "Only");
169+
170+
var graph = NavigationGraphBuilder.Build(_root);
171+
var only = graph.ResolveByUrl("/only")!;
172+
173+
var (prev, next) = graph.GetPrevNext(only);
174+
175+
Assert.Null(prev);
176+
Assert.Null(next);
177+
}
178+
179+
[Fact]
180+
public void GetBreadcrumb_WalksUpFromLeafToTopLevel()
181+
{
182+
WriteMd("docs/theming/tokens.md", "Tokens");
183+
184+
var graph = NavigationGraphBuilder.Build(_root);
185+
var leaf = graph.ResolveByUrl("/docs/theming/tokens")!;
186+
var trail = graph.GetBreadcrumb(leaf);
187+
188+
Assert.Equal(new[] { "Docs", "Theming", "Tokens" }, trail.Select(n => n.Title));
189+
}
190+
191+
[Fact]
192+
public void ResolveByUrl_IsCaseInsensitiveAndTrailingSlashTolerant()
193+
{
194+
WriteMd("Button.md", "Button");
195+
196+
var graph = NavigationGraphBuilder.Build(_root);
197+
198+
Assert.NotNull(graph.ResolveByUrl("/button"));
199+
Assert.NotNull(graph.ResolveByUrl("/BUTTON"));
200+
Assert.NotNull(graph.ResolveByUrl("/button/"));
201+
Assert.NotNull(graph.ResolveByUrl("button"));
202+
}
203+
}

tests/ShellDocs.Tests/ScaffoldingTests.cs

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,11 @@
1-
using ShellDocs.Core;
21
using ShellDocs.Markdown;
32
using ShellDocs.Templates;
43
using Xunit;
54

65
namespace ShellDocs.Tests;
76

8-
// Smoke tests — verify each package's public surface loads and its trivial APIs behave.
9-
// Real tests land branch-by-branch.
107
public class ScaffoldingTests
118
{
12-
[Fact]
13-
public void NavigationNode_HoldsRequiredFields()
14-
{
15-
var node = new NavigationNode { Url = "/docs/hello", Title = "Hello" };
16-
Assert.Equal("/docs/hello", node.Url);
17-
Assert.Equal("Hello", node.Title);
18-
}
19-
209
[Fact]
2110
public void MarkdownPipelineFactory_ProducesUsablePipeline()
2211
{

0 commit comments

Comments
 (0)