diff --git a/tests/Elastic.Documentation.Configuration.Tests/.snapshots/ConfigurationFileApiTests/EmitsError_WhenRepositoryNotInOrgSlashRepoForm.08363ed5.verified.txt b/tests/Elastic.Documentation.Configuration.Tests/.snapshots/ConfigurationFileApiTests/EmitsError_WhenRepositoryNotInOrgSlashRepoForm.08363ed5.verified.txt new file mode 100644 index 0000000000..c67e2508b3 --- /dev/null +++ b/tests/Elastic.Documentation.Configuration.Tests/.snapshots/ConfigurationFileApiTests/EmitsError_WhenRepositoryNotInOrgSlashRepoForm.08363ed5.verified.txt @@ -0,0 +1 @@ +error {repo}/docs/_docset.yml 'repository: no-slash' for API 'elasticsearch' must be in 'org/repo' form, e.g. 'elastic/elasticsearch-specification'. diff --git a/tests/Elastic.Documentation.Configuration.Tests/.snapshots/ConfigurationFileApiTests/EmitsError_WhenRepositoryNotInOrgSlashRepoForm.af33dc75.verified.txt b/tests/Elastic.Documentation.Configuration.Tests/.snapshots/ConfigurationFileApiTests/EmitsError_WhenRepositoryNotInOrgSlashRepoForm.af33dc75.verified.txt new file mode 100644 index 0000000000..beb6efa123 --- /dev/null +++ b/tests/Elastic.Documentation.Configuration.Tests/.snapshots/ConfigurationFileApiTests/EmitsError_WhenRepositoryNotInOrgSlashRepoForm.af33dc75.verified.txt @@ -0,0 +1 @@ +error {repo}/docs/_docset.yml 'repository: trailing-slash/' for API 'elasticsearch' must be in 'org/repo' form, e.g. 'elastic/elasticsearch-specification'. diff --git a/tests/Elastic.Documentation.Configuration.Tests/.snapshots/ConfigurationFileApiTests/EmitsError_WhenRepositoryNotInOrgSlashRepoForm.c6f1a4ef.verified.txt b/tests/Elastic.Documentation.Configuration.Tests/.snapshots/ConfigurationFileApiTests/EmitsError_WhenRepositoryNotInOrgSlashRepoForm.c6f1a4ef.verified.txt new file mode 100644 index 0000000000..04ba83e1a6 --- /dev/null +++ b/tests/Elastic.Documentation.Configuration.Tests/.snapshots/ConfigurationFileApiTests/EmitsError_WhenRepositoryNotInOrgSlashRepoForm.c6f1a4ef.verified.txt @@ -0,0 +1 @@ +error {repo}/docs/_docset.yml 'repository: /leading-slash' for API 'elasticsearch' must be in 'org/repo' form, e.g. 'elastic/elasticsearch-specification'. diff --git a/tests/Elastic.Documentation.Configuration.Tests/ApiConfigurationTests.cs b/tests/Elastic.Documentation.Configuration.Tests/ApiConfigurationTests.cs index d0ee0d2f42..56ba61e377 100644 --- a/tests/Elastic.Documentation.Configuration.Tests/ApiConfigurationTests.cs +++ b/tests/Elastic.Documentation.Configuration.Tests/ApiConfigurationTests.cs @@ -433,7 +433,13 @@ public void EmitsError_WhenSpecMissing() var (config, collector) = CreateConfiguration(docSetFile); - collector.Errors.Should().Be(1); + collector + .Captured + .ToSnapshotText() + .Should() + .MatchInlineSnapshot( + "error {repo}/docs/_docset.yml API 'elasticsearch' is missing required 'spec:'. Its basename is required to resolve the remote version index, even when the file is not present locally." + ); config.ApiConfigurations.Should().BeNull(); } @@ -450,7 +456,13 @@ public void EmitsError_WhenSpecEscapesDocumentationSourceDirectory() var (config, collector) = CreateConfiguration(docSetFile); - collector.Errors.Should().Be(1); + collector + .Captured + .ToSnapshotText() + .Should() + .MatchInlineSnapshot( + "error {repo}/docs/_docset.yml 'spec: ../../outside.json' for API 'elasticsearch' escapes the documentation source directory." + ); config.ApiConfigurations.Should().BeNull(); } @@ -467,7 +479,13 @@ public void EmitsError_WhenProductMissing() var (config, collector) = CreateConfiguration(docSetFile); - collector.Errors.Should().Be(1); + collector + .Captured + .ToSnapshotText() + .Should() + .MatchInlineSnapshot( + "error {repo}/docs/_docset.yml API 'elasticsearch' is missing required 'product:'. It must match a product id defined in products.yml." + ); config.ApiConfigurations.Should().BeNull(); } @@ -487,7 +505,13 @@ public void EmitsError_WhenProductUnknown() var (config, collector) = CreateConfiguration(docSetFile); - collector.Errors.Should().Be(1); + collector + .Captured + .ToSnapshotText() + .Should() + .MatchInlineSnapshot( + "error {repo}/docs/_docset.yml Unknown 'product: not-a-product' for API 'elasticsearch'. It must be a product id defined in products.yml." + ); config.ApiConfigurations.Should().BeNull(); } @@ -591,7 +615,13 @@ public void EmitsError_WhenCatalogCategoryUnknown() var (config, collector) = CreateConfiguration(docSetFile); - collector.Errors.Should().Be(1); + collector + .Captured + .ToSnapshotText() + .Should() + .MatchInlineSnapshot( + "error {repo}/docs/_docset.yml Unknown catalog category 'eck' for API 'elasticsearch'. Valid categories: ece, ess, self, serverless (or ech for ess)." + ); config.ApiConfigurations!["elasticsearch"].CatalogCategories.Should().Equal("self"); } @@ -657,8 +687,8 @@ public void EmitsError_WhenRepositoryNotInOrgSlashRepoForm(string repository) var (config, collector) = CreateConfiguration(docSetFile); - collector.Errors.Should().Be(1); - config.ApiConfigurations.Should().BeNull(); + // the output differs per case, so it is a file snapshot (keyed by the arguments) rather than a shared inline literal + collector.Captured.ToSnapshotText().Should().MatchSnapshot(); } [Test] @@ -677,7 +707,13 @@ public void EmitsError_WhenMultipleEntries() var (config, collector) = CreateConfiguration(docSetFile); - collector.Errors.Should().Be(1); + collector + .Captured + .ToSnapshotText() + .Should() + .MatchInlineSnapshot( + "error {repo}/docs/_docset.yml API configuration for 'elasticsearch' must have exactly one entry, found 2." + ); config.ApiConfigurations.Should().BeNull(); } @@ -705,7 +741,13 @@ public void EmitsError_WhenChildFileMissing() var (config, collector) = CreateConfiguration(docSetFile); - collector.Errors.Should().Be(1); + collector + .Captured + .ToSnapshotText() + .Should() + .MatchInlineSnapshot( + "error {repo}/docs/_docset.yml Child page 'missing.md' for API 'elasticsearch' does not exist under 'api/elasticsearch/'." + ); config.ApiConfigurations!["elasticsearch"].Children.Should().BeEmpty(); } @@ -733,7 +775,13 @@ public void EmitsError_WhenChildPathEscapesApiKeyDirectory() var (config, collector) = CreateConfiguration(docSetFile); - collector.Errors.Should().Be(1); + collector + .Captured + .ToSnapshotText() + .Should() + .MatchInlineSnapshot( + "error {repo}/docs/_docset.yml Child page '../../outside.md' for API 'elasticsearch' escapes 'api/elasticsearch/'." + ); config.ApiConfigurations!["elasticsearch"].Children.Should().BeEmpty(); } @@ -761,7 +809,13 @@ public void EmitsError_WhenChildFileUsesSupplementalName() var (config, collector) = CreateConfiguration(docSetFile, extraMarkdownFiles: ["op-search.md"]); - collector.Errors.Should().Be(1); + collector + .Captured + .ToSnapshotText() + .Should() + .MatchInlineSnapshot( + "error {repo}/docs/_docset.yml Child page 'op-search.md' for API 'elasticsearch' uses a supplemental file name (op-*.md / tag-*.md). Those files are auto-discovered and cannot be listed under children:." + ); config.ApiConfigurations!["elasticsearch"].Children.Should().BeEmpty(); } @@ -853,14 +907,14 @@ public void ApiContentDirectory_IsSetToApiKeyFolder() private static readonly string[] DefaultProductIds = ["elasticsearch", "kibana"]; - private static (ConfigurationFile Config, DiagnosticsCollector Collector) CreateConfiguration( + private static (ConfigurationFile Config, CapturingDiagnosticsCollector Collector) CreateConfiguration( DocumentationSetFile docSet, string[]? extraProducts = null, bool withLocalSpecFile = true, string[]? extraMarkdownFiles = null ) { - var collector = new DiagnosticsCollector([]); + var collector = new CapturingDiagnosticsCollector(); var root = Paths.WorkingDirectoryRoot.FullName; var configFilePath = Path.Join(root, "docs", "_docset.yml"); var files = new Dictionary diff --git a/tests/Elastic.Documentation.Configuration.Tests/ConfigurationFileCtaTests.cs b/tests/Elastic.Documentation.Configuration.Tests/ConfigurationFileCtaTests.cs index 8726dd4934..94a813742a 100644 --- a/tests/Elastic.Documentation.Configuration.Tests/ConfigurationFileCtaTests.cs +++ b/tests/Elastic.Documentation.Configuration.Tests/ConfigurationFileCtaTests.cs @@ -237,8 +237,11 @@ public async Task LoadAndResolve_PageClaimedByTwoDefaults_EmitsError() recorder .Diagnostics + .ToSnapshotText() .Should() - .Contain(d => d.Severity == Severity.Error && d.Message.Contains("observability") && d.Message.Contains("security")); + .MatchInlineSnapshot( + "error /docs/section-b/toc.yml 'shared/page.md' is registered with default CTA 'observability' and 'security'. Each page can only have one default CTA." + ); } [Test] @@ -260,7 +263,12 @@ public async Task Constructor_UnknownTocDefaultCta_EmitsError() var (_, diagnostics) = await CreateConfigurationWithDiagnostics(docSet); - diagnostics.Should().ContainSingle(d => d.Severity == Severity.Error).Which.Message.Should().Contain("does-not-exist"); + diagnostics + .ToSnapshotText() + .Should() + .MatchInlineSnapshot( + "error /docs/docset.yml 'default_cta: does-not-exist' on page 'solutions/observability/apps/apm.md' does not match any 'cta' template in docset.yml." + ); } [Test] diff --git a/tests/Elastic.Documentation.Configuration.Tests/ConfigurationFileStorybookRegistryTests.cs b/tests/Elastic.Documentation.Configuration.Tests/ConfigurationFileStorybookRegistryTests.cs index 01fc923373..0685df6485 100644 --- a/tests/Elastic.Documentation.Configuration.Tests/ConfigurationFileStorybookRegistryTests.cs +++ b/tests/Elastic.Documentation.Configuration.Tests/ConfigurationFileStorybookRegistryTests.cs @@ -42,7 +42,7 @@ public void SetVariable_ResolvesToEnvironmentValue_AndExposesDefaultAsFallback() [Test] public void DisallowedVariable_IsLeftLiteral_AndWarns() { - var collector = new DiagnosticsCollector([]); + var collector = new CapturingDiagnosticsCollector(); var config = CreateConfiguration( "${AWS_SECRET_ACCESS_KEY:-fallback}", new MockEnvironment { ["AWS_SECRET_ACCESS_KEY"] = "super-secret" }, @@ -51,7 +51,13 @@ public void DisallowedVariable_IsLeftLiteral_AndWarns() config.StorybookRegistry.Should().Be("${AWS_SECRET_ACCESS_KEY:-fallback}"); config.StorybookRegistry.Should().NotContain("super-secret"); - collector.Warnings.Should().Be(1, "a disallowed interpolation variable must emit exactly one warning"); + collector + .Captured + .ToSnapshotText() + .Should() + .MatchInlineSnapshot( + "warning {repo}/docs/_docset.yml 'storybook.registry' references environment variable 'AWS_SECRET_ACCESS_KEY' which is not allow-listed for interpolation and is left literal. Allowed: KIBANA_STORYBOOK_REGISTRY." + ); } private static ConfigurationFile CreateConfiguration( diff --git a/tests/Elastic.Documentation.Configuration.Tests/CreateNavigationFileTests.cs b/tests/Elastic.Documentation.Configuration.Tests/CreateNavigationFileTests.cs index 32c1294aef..ca51d9fbea 100644 --- a/tests/Elastic.Documentation.Configuration.Tests/CreateNavigationFileTests.cs +++ b/tests/Elastic.Documentation.Configuration.Tests/CreateNavigationFileTests.cs @@ -55,10 +55,18 @@ public void ConsecutiveSiblingPrivateEntries_BothRemoved() var result = provider.CreateNavigationFile(config); var output = fileSystem.File.ReadAllText(result.FullName); - output.Should().NotContain("private-a://"); - output.Should().NotContain("private-b://"); - output.Should().Contain("public-repo://section-a"); - output.Should().Contain("public-repo-two://section-d"); + output.Should().MatchInlineSnapshot( + """ + toc: + - toc: docs-content://getting-started + path_prefix: getting-started + children: + - toc: public-repo://section-a + path_prefix: section-a + - toc: public-repo-two://section-d + path_prefix: section-d + """ + ); } [Test] @@ -92,10 +100,21 @@ public void DeeplyNestedConsecutivePrivateEntries_BothRemoved() var result = provider.CreateNavigationFile(config); var output = fileSystem.File.ReadAllText(result.FullName); - output.Should().NotContain("private-a://"); - output.Should().NotContain("private-b://"); - output.Should().Contain("public-repo://leaf-a"); - output.Should().Contain("public-repo-two://leaf-d"); + output.Should().MatchInlineSnapshot( + """ + toc: + - toc: docs-content://top + path_prefix: top + children: + - toc: docs-content://mid + path_prefix: mid + children: + - toc: public-repo://leaf-a + path_prefix: leaf-a + - toc: public-repo-two://leaf-d + path_prefix: leaf-d + """ + ); } [Test] @@ -125,9 +144,18 @@ public void PrivateRepoWithChildren_EntryRemovedChildrenReindented() var result = provider.CreateNavigationFile(config); var output = fileSystem.File.ReadAllText(result.FullName); - output.Should().NotContain("private-a://"); - output.Should().Contain("public-child://nested"); - output.Should().Contain("public-repo://after"); + output.Should().MatchInlineSnapshot( + """ + toc: + - toc: docs-content://top + path_prefix: top + children: + - toc: public-child://nested + path_prefix: nested + - toc: public-repo://after + path_prefix: after + """ + ); } [Test] @@ -156,9 +184,16 @@ public void PublicEntriesBetweenPrivateEntries_Preserved() var result = provider.CreateNavigationFile(config); var output = fileSystem.File.ReadAllText(result.FullName); - output.Should().NotContain("private-a://"); - output.Should().NotContain("private-b://"); - output.Should().Contain("public-repo://middle"); + output.Should().MatchInlineSnapshot( + """ + toc: + - toc: docs-content://top + path_prefix: top + children: + - toc: public-repo://middle + path_prefix: middle + """ + ); } [Test] @@ -187,11 +222,17 @@ public void IslandProperty_OnPublicEntry_SurvivesPrivateRepoStripping() var output = fileSystem.File.ReadAllText(result.FullName); // The public entry and its island: true property are preserved - output.Should().Contain("public-repo://reference"); - output.Should().Contain("island: true", "island: true on a public entry must survive private-repo stripping"); - output.Should().Contain("public-child://nested"); - // The private entry is removed - output.Should().NotContain("private-a://"); + output.Should().MatchInlineSnapshot( + """ + toc: + - toc: public-repo://reference + path_prefix: reference + island: true + children: + - toc: public-child://nested + path_prefix: reference/nested + """ + ); } [Test] @@ -219,11 +260,15 @@ public void IslandProperty_OnPrivateEntry_IsRemovedWithEntry() var output = fileSystem.File.ReadAllText(result.FullName); // The private entry and its island: true property are removed - output.Should().NotContain("private-a://"); - output.Should().NotContain("island: true", "island: true on a private entry is removed with the entry"); - // Public entries are preserved - output.Should().Contain("public-repo://reference"); - output.Should().Contain("public-after://other"); + output.Should().MatchInlineSnapshot( + """ + toc: + - toc: public-repo://reference + path_prefix: reference + - toc: public-after://other + path_prefix: other + """ + ); } [Test] diff --git a/tests/Elastic.Documentation.Configuration.Tests/DocumentationSetFileTests.cs b/tests/Elastic.Documentation.Configuration.Tests/DocumentationSetFileTests.cs index fbcd04d4f6..85f2a4a820 100644 --- a/tests/Elastic.Documentation.Configuration.Tests/DocumentationSetFileTests.cs +++ b/tests/Elastic.Documentation.Configuration.Tests/DocumentationSetFileTests.cs @@ -118,17 +118,13 @@ public void DeserializesFileReference() var result = Deserialize(yaml); - result.TableOfContents.Should().HaveCount(2); - result.TableOfContents.ElementAt(0).Should().BeOfType().Which.PathRelativeToDocumentationSet.Should().Be("index.md"); - result - .TableOfContents - .ElementAt(1) + TocDump + .ToText(result.TableOfContents) .Should() - .BeOfType() - .Which - .PathRelativeToDocumentationSet - .Should() - .Be("getting-started.md"); + .MatchInlineSnapshot(""" + IndexFileRef index.md + FileRef getting-started.md + """); } [Test] @@ -146,11 +142,16 @@ public void DeserializesHiddenFileReference() var result = Deserialize(yaml); - result.TableOfContents.Should().HaveCount(3); - result.TableOfContents.ElementAt(0).Should().BeOfType().Which.Hidden.Should().BeFalse(); - result.TableOfContents.ElementAt(1).Should().BeOfType().Which.Hidden.Should().BeTrue(); - result.TableOfContents.ElementAt(1).Should().BeOfType().Which.PathRelativeToDocumentationSet.Should().Be("404.md"); - result.TableOfContents.ElementAt(2).Should().BeOfType().Which.Hidden.Should().BeTrue(); + TocDump + .ToText(result.TableOfContents) + .Should() + .MatchInlineSnapshot( + """ + IndexFileRef index.md + FileRef 404.md [hidden] + FileRef developer-notes.md [hidden] + """ + ); } [Test] @@ -169,12 +170,16 @@ public void DeserializesFolderReference() var result = Deserialize(yaml); - result.TableOfContents.Should().HaveCount(1); - var folder = result.TableOfContents.ElementAt(0).Should().BeOfType().Subject; - folder.PathRelativeToDocumentationSet.Should().Be("contribute"); - folder.Children.Should().HaveCount(2); - folder.Children.ElementAt(0).Should().BeOfType().Which.PathRelativeToDocumentationSet.Should().Be("index.md"); - folder.Children.ElementAt(1).Should().BeOfType().Which.PathRelativeToDocumentationSet.Should().Be("locally.md"); + TocDump + .ToText(result.TableOfContents) + .Should() + .MatchInlineSnapshot( + """ + FolderRef contribute + IndexFileRef index.md + FileRef locally.md + """ + ); } [Test] @@ -191,17 +196,13 @@ public void DeserializesTocReference() var result = Deserialize(yaml); - result.TableOfContents.Should().HaveCount(2); - result.TableOfContents.ElementAt(0).Should().BeOfType(); - result - .TableOfContents - .ElementAt(1) - .Should() - .BeOfType() - .Which - .PathRelativeToDocumentationSet + TocDump + .ToText(result.TableOfContents) .Should() - .Be("development"); + .MatchInlineSnapshot(""" + IndexFileRef index.md + IsolatedTableOfContentsRef development + """); } [Test] @@ -221,12 +222,16 @@ public void DeserializesCrossLinkReference() var result = Deserialize(yaml); - result.TableOfContents.Should().HaveCount(2); - var fileWithChildren = result.TableOfContents.ElementAt(1).Should().BeOfType().Subject; - fileWithChildren.Children.Should().HaveCount(1); - var crosslink = fileWithChildren.Children.ElementAt(0).Should().BeOfType().Subject; - crosslink.Title.Should().Be("Getting Started Guide"); - crosslink.CrossLinkUri.ToString().Should().Be("docs-content://get-started/introduction.md"); + TocDump + .ToText(result.TableOfContents) + .Should() + .MatchInlineSnapshot( + """ + IndexFileRef index.md + FileRef cross-links.md + CrossLinkRef docs-content://get-started/introduction.md title="Getting Started Guide" + """ + ); } [Test] @@ -249,19 +254,19 @@ public void DeserializesNestedStructure() var result = Deserialize(yaml); - result.TableOfContents.Should().HaveCount(1); - var topFolder = result.TableOfContents.ElementAt(0).Should().BeOfType().Subject; - topFolder.PathRelativeToDocumentationSet.Should().Be("configure"); - topFolder.Children.Should().HaveCount(2); - - topFolder.Children.ElementAt(0).Should().BeOfType().Which.PathRelativeToDocumentationSet.Should().Be("index.md"); - - var nestedFolder = topFolder.Children.ElementAt(1).Should().BeOfType().Subject; - nestedFolder.PathRelativeToDocumentationSet.Should().Be("site"); - nestedFolder.Children.Should().HaveCount(3); - nestedFolder.Children.ElementAt(0).Should().BeOfType().Which.PathRelativeToDocumentationSet.Should().Be("index.md"); - nestedFolder.Children.ElementAt(1).Should().BeOfType().Which.PathRelativeToDocumentationSet.Should().Be("content.md"); - nestedFolder.Children.ElementAt(2).Should().BeOfType().Which.PathRelativeToDocumentationSet.Should().Be("navigation.md"); + TocDump + .ToText(result.TableOfContents) + .Should() + .MatchInlineSnapshot( + """ + FolderRef configure + IndexFileRef index.md + FolderRef site + IndexFileRef index.md + FileRef content.md + FileRef navigation.md + """ + ); } [Test] @@ -320,62 +325,24 @@ public void DeserializesCompleteDocsetYaml() result.Features.PrimaryNav.Should().BeFalse(); result.Api.Should().HaveCount(2); - // Assert TOC structure - 4 root items - result.TableOfContents.Should().HaveCount(4); - - // First item: simple file reference - var firstItem = result.TableOfContents.ElementAt(0).Should().BeOfType().Subject; - firstItem.PathRelativeToDocumentationSet.Should().Be("index.md"); - firstItem.Hidden.Should().BeFalse(); - firstItem.Children.Should().BeEmpty(); - - // Second item: hidden file reference - var secondItem = result.TableOfContents.ElementAt(1).Should().BeOfType().Subject; - secondItem.PathRelativeToDocumentationSet.Should().Be("404.md"); - secondItem.Hidden.Should().BeTrue(); - secondItem.Children.Should().BeEmpty(); - - // Third item: folder with a deeply nested structure - var configureFolder = result.TableOfContents.ElementAt(2).Should().BeOfType().Subject; - configureFolder.PathRelativeToDocumentationSet.Should().Be("configure"); - configureFolder.Children.Should().HaveCount(3); - - // First child: file reference - var configureIndexFile = configureFolder.Children.ElementAt(0).Should().BeOfType().Subject; - configureIndexFile.PathRelativeToDocumentationSet.Should().Be("index.md"); - configureIndexFile.Hidden.Should().BeFalse(); - - // Second child: nested folder with 3 files - var siteFolder = configureFolder.Children.ElementAt(1).Should().BeOfType().Subject; - siteFolder.PathRelativeToDocumentationSet.Should().Be("site"); - siteFolder.Children.Should().HaveCount(3); - - // Assert nested folder's children - var siteIndexFile = siteFolder.Children.ElementAt(0).Should().BeOfType().Subject; - siteIndexFile.PathRelativeToDocumentationSet.Should().Be("index.md"); - - var contentFile = siteFolder.Children.ElementAt(1).Should().BeOfType().Subject; - contentFile.PathRelativeToDocumentationSet.Should().Be("content.md"); - - var navigationFile = siteFolder.Children.ElementAt(2).Should().BeOfType().Subject; - navigationFile.PathRelativeToDocumentationSet.Should().Be("navigation.md"); - - // Third child: file with crosslink child - var pageFile = configureFolder.Children.ElementAt(2).Should().BeOfType().Subject; - pageFile.PathRelativeToDocumentationSet.Should().Be("page.md"); - pageFile.Children.Should().HaveCount(1); - - // Assert crosslink reference as a child of page.md - var crosslink = pageFile.Children.ElementAt(0).Should().BeOfType().Subject; - crosslink.Title.Should().Be("Getting Started Guide"); - crosslink.CrossLinkUri.ToString().Should().Be("docs-content://get-started/introduction.md"); - crosslink.Hidden.Should().BeFalse(); - crosslink.Children.Should().BeEmpty(); - - // Fourth item: toc reference - var tocRef = result.TableOfContents.ElementAt(3).Should().BeOfType().Subject; - tocRef.PathRelativeToDocumentationSet.Should().Be("development"); - tocRef.Children.Should().BeEmpty(); + TocDump + .ToText(result.TableOfContents) + .Should() + .MatchInlineSnapshot( + """ + IndexFileRef index.md + FileRef 404.md [hidden] + FolderRef configure + IndexFileRef index.md + FolderRef site + IndexFileRef index.md + FileRef content.md + FileRef navigation.md + FileRef page.md + CrossLinkRef docs-content://get-started/introduction.md title="Getting Started Guide" + IsolatedTableOfContentsRef development + """ + ); } [Test] @@ -395,13 +362,17 @@ public void DeserializesFileWithChildren() var result = Deserialize(yaml); - result.TableOfContents.Should().HaveCount(1); - var guide = result.TableOfContents.ElementAt(0).Should().BeOfType().Subject; - guide.PathRelativeToDocumentationSet.Should().Be("guide.md"); - guide.Children.Should().HaveCount(3); - guide.Children.ElementAt(0).Should().BeOfType().Which.PathRelativeToDocumentationSet.Should().Be("chapter1.md"); - guide.Children.ElementAt(1).Should().BeOfType().Which.PathRelativeToDocumentationSet.Should().Be("chapter2.md"); - guide.Children.ElementAt(2).Should().BeOfType().Which.PathRelativeToDocumentationSet.Should().Be("chapter3.md"); + TocDump + .ToText(result.TableOfContents) + .Should() + .MatchInlineSnapshot( + """ + FileRef guide.md + FileRef chapter1.md + FileRef chapter2.md + FileRef chapter3.md + """ + ); } [Test] @@ -420,12 +391,16 @@ public void DeserializesFileWithNestedPathsAsChildren() var result = Deserialize(yaml); - result.TableOfContents.Should().HaveCount(1); - var guide = result.TableOfContents.ElementAt(0).Should().BeOfType().Subject; - guide.PathRelativeToDocumentationSet.Should().Be("api/guide.md"); - guide.Children.Should().HaveCount(2); - guide.Children.ElementAt(0).Should().BeOfType().Which.PathRelativeToDocumentationSet.Should().Be("api/section1.md"); - guide.Children.ElementAt(1).Should().BeOfType().Which.PathRelativeToDocumentationSet.Should().Be("api/section2.md"); + TocDump + .ToText(result.TableOfContents) + .Should() + .MatchInlineSnapshot( + """ + FileRef api/guide.md + FileRef api/section1.md + FileRef api/section2.md + """ + ); } [Test] @@ -479,10 +454,13 @@ public void DeserializesCrossLinkWithoutTitle() var result = Deserialize(yaml); - var file = result.TableOfContents.ElementAt(0).Should().BeOfType().Subject; - var crosslink = file.Children.ElementAt(0).Should().BeOfType().Subject; - crosslink.CrossLinkUri.ToString().Should().Be("docs-content://get-started.md/"); // URI normalization adds trailing slash - crosslink.Title.Should().BeNull(); + TocDump + .ToText(result.TableOfContents) + .Should() + .MatchInlineSnapshot(""" + IndexFileRef index.md + CrossLinkRef docs-content://get-started.md/ + """); } [Test] @@ -501,11 +479,17 @@ public void DeserializesMixedHiddenAndVisibleItems() var result = Deserialize(yaml); - result.TableOfContents.Should().HaveCount(4); - result.TableOfContents.ElementAt(0).Should().BeOfType().Which.Hidden.Should().BeFalse(); - result.TableOfContents.ElementAt(1).Should().BeOfType().Which.Hidden.Should().BeTrue(); - result.TableOfContents.ElementAt(2).Should().BeOfType().Which.Hidden.Should().BeFalse(); - result.TableOfContents.ElementAt(3).Should().BeOfType().Which.Hidden.Should().BeTrue(); + TocDump + .ToText(result.TableOfContents) + .Should() + .MatchInlineSnapshot( + """ + IndexFileRef index.md + FileRef _internal.md [hidden] + FileRef public.md + FileRef _draft.md [hidden] + """ + ); } [Test] @@ -527,13 +511,17 @@ public void DeserializesDeeplyNestedFileWithChildren() var result = Deserialize(yaml); - var guide = result.TableOfContents.ElementAt(0).Should().BeOfType().Subject; - var chapter1 = guide.Children.ElementAt(0).Should().BeOfType().Subject; - var section1 = chapter1.Children.ElementAt(0).Should().BeOfType().Subject; - var subsection1 = section1.Children.ElementAt(0).Should().BeOfType().Subject; - - subsection1.PathRelativeToDocumentationSet.Should().Be("subsection1.md"); - subsection1.Children.Should().BeEmpty(); + TocDump + .ToText(result.TableOfContents) + .Should() + .MatchInlineSnapshot( + """ + FileRef guide.md + FileRef chapter1.md + FileRef section1.md + FileRef subsection1.md + """ + ); } [Test] @@ -598,11 +586,18 @@ public void DeserializesFolderWithMixedChildren() var result = Deserialize(yaml); - var apiFolder = result.TableOfContents.ElementAt(0).Should().BeOfType().Subject; - apiFolder.Children.Should().HaveCount(3); - apiFolder.Children.ElementAt(0).Should().BeOfType(); - apiFolder.Children.ElementAt(1).Should().BeOfType(); - apiFolder.Children.ElementAt(2).Should().BeOfType(); + TocDump + .ToText(result.TableOfContents) + .Should() + .MatchInlineSnapshot( + """ + FolderRef api + IndexFileRef index.md + FolderRef rest + IndexFileRef index.md + FileRef overview.md + """ + ); } [Test] @@ -626,14 +621,14 @@ public void LoadAndResolve_UnknownDocsetKey_EmitsErrorWithLocation() var collector = new CapturingDiagnosticsCollector(); _ = DocumentationSetFile.LoadAndResolve(collector, docsetPath, new ScopedFileSystem(fileSystem, "/docs")); - collector.Warnings.Should().Be(0); - collector.Errors.Should().Be(1); - var diagnostic = collector.Captured.Should().ContainSingle().Subject; - diagnostic.Severity.Should().Be(Severity.Error); - diagnostic.File.Should().EndWith("docset.yml"); - diagnostic.Line.Should().Be(2); - diagnostic.Column.Should().Be(1); - diagnostic.Message.Should().Contain("Unknown key 'totally_unknown_key'"); + collector + .Captured + .ToSnapshotText() + .Should() + .MatchInlineSnapshot( + "error /docs/docset.yml:2 Unknown key 'totally_unknown_key'. Valid keys: api, branding, codex, cross_links, cta, default_cta, description, dev_docs, display_name, exclude, extensions, features, …." + ); + collector.Captured.Should().ContainSingle().Which.Column.Should().Be(1); } [Test] @@ -664,14 +659,14 @@ public void LoadAndResolve_UnknownNestedTocKey_EmitsErrorWithLocation() var collector = new CapturingDiagnosticsCollector(); _ = DocumentationSetFile.LoadAndResolve(collector, docsetPath, new ScopedFileSystem(fileSystem, "/docs")); - collector.Warnings.Should().Be(0); - collector.Errors.Should().Be(1); - var diagnostic = collector.Captured.Should().ContainSingle().Subject; - diagnostic.Severity.Should().Be(Severity.Error); - diagnostic.File.Should().EndWith("toc.yml"); - diagnostic.Line.Should().Be(1); - diagnostic.Column.Should().Be(1); - diagnostic.Message.Should().Contain("Unknown key 'totally_unknown_key'"); + collector + .Captured + .ToSnapshotText() + .Should() + .MatchInlineSnapshot( + "error /docs/nested/toc.yml:1 Unknown key 'totally_unknown_key'. Valid keys: default_cta, island, project, suppress, toc." + ); + collector.Captured.Should().ContainSingle().Which.Column.Should().Be(1); } [Test] @@ -755,88 +750,24 @@ public void LoadAndResolveResolvesIsolatedTocReferences() // Verify TOC references have been preserved (not flattened) // We have 3 top-level items: index.md, development TOC, and guides folder - result.TableOfContents.Should().HaveCount(3); - - // First item: file from main docset - result.TableOfContents.ElementAt(0).Should().BeOfType().Which.PathRelativeToDocumentationSet.Should().Be("index.md"); - - // Second item: development TOC (preserved as IsolatedTableOfContentsRef with resolved children) - var developmentToc = result.TableOfContents.ElementAt(1).Should().BeOfType().Subject; - developmentToc.PathRelativeToDocumentationSet.Should().Be("development"); - developmentToc.Children.Should().HaveCount(3, "should have index, contributing file, and internals folder"); - - developmentToc - .Children - .ElementAt(0) - .Should() - .BeOfType() - .Which - .PathRelativeToDocumentationSet - .Should() - .Be("development/index.md", "TOC path should be prepended"); - - developmentToc - .Children - .ElementAt(1) - .Should() - .BeOfType() - .Which - .PathRelativeToDocumentationSet - .Should() - .Be("development/contributing.md"); - - var internalsFolder = developmentToc.Children.ElementAt(2).Should().BeOfType().Subject; - internalsFolder.PathRelativeToDocumentationSet.Should().Be("development/internals"); - internalsFolder.Children.Should().HaveCount(1); - internalsFolder - .Children - .ElementAt(0) - .Should() - .BeOfType() - .Which - .PathRelativeToDocumentationSet - .Should() - .Be("development/internals/architecture.md"); - - // Third item: guides folder (preserved with its children including nested advanced TOC) - var guidesFolder = result.TableOfContents.ElementAt(2).Should().BeOfType().Subject; - guidesFolder.PathRelativeToDocumentationSet.Should().Be("guides"); - guidesFolder.Children.Should().HaveCount(2, "should have getting-started file and advanced TOC"); - - guidesFolder - .Children - .ElementAt(0) - .Should() - .BeOfType() - .Which - .PathRelativeToDocumentationSet - .Should() - .Be("guides/getting-started.md"); - - // Advanced TOC preserved as IsolatedTableOfContentsRef within guides folder - var advancedToc = guidesFolder.Children.ElementAt(1).Should().BeOfType().Subject; - advancedToc.PathRelativeToDocumentationSet.Should().Be("guides/advanced"); - advancedToc.Children.Should().HaveCount(2); - - advancedToc - .Children - .ElementAt(0) - .Should() - .BeOfType() - .Which - .PathRelativeToDocumentationSet - .Should() - .Be("guides/advanced/index.md"); - - advancedToc - .Children - .ElementAt(1) - .Should() - .BeOfType() - .Which - .PathRelativeToDocumentationSet + TocDump + .ToText(result.TableOfContents) .Should() - .Be("guides/advanced/patterns.md"); + .MatchInlineSnapshot( + """ + IndexFileRef index.md + IsolatedTableOfContentsRef development + IndexFileRef development/index.md (container-relative: index.md) + FileRef development/contributing.md (container-relative: contributing.md) + FolderRef development/internals (container-relative: internals) + FileRef development/internals/architecture.md (container-relative: internals/architecture.md) + FolderRef guides + FileRef guides/getting-started.md + IsolatedTableOfContentsRef guides/advanced + IndexFileRef guides/advanced/index.md (container-relative: index.md) + FileRef guides/advanced/patterns.md (container-relative: patterns.md) + """ + ); } [Test] @@ -866,44 +797,19 @@ public void LoadAndResolvePrependsParentPathsToFileReferences() var collector = new DiagnosticsCollector([]); var result = DocumentationSetFile.LoadAndResolve(collector, docsetPath, new ScopedFileSystem(fileSystem, "/docs")); - result.TableOfContents.Should().HaveCount(2); - - // First item: file with nested children - var guide = result.TableOfContents.ElementAt(0).Should().BeOfType().Subject; - guide.PathRelativeToDocumentationSet.Should().Be("guide.md"); - guide.Children.Should().HaveCount(1); - - var chapter1 = guide.Children.ElementAt(0).Should().BeOfType().Subject; - chapter1.PathRelativeToDocumentationSet.Should().Be("chapter1.md", "children of files stay in the same directory"); - chapter1.Children.Should().HaveCount(1); - - var section1 = chapter1.Children.ElementAt(0).Should().BeOfType().Subject; - section1.PathRelativeToDocumentationSet.Should().Be("section1.md", "children of files stay in the same directory"); - - // Second item: folder with children - var apiFolder = result.TableOfContents.ElementAt(1).Should().BeOfType().Subject; - apiFolder.PathRelativeToDocumentationSet.Should().Be("api"); - apiFolder.Children.Should().HaveCount(2); - - apiFolder - .Children - .ElementAt(0) - .Should() - .BeOfType() - .Which - .PathRelativeToDocumentationSet - .Should() - .Be("api/index.md", "folder path 'api' should be prepended"); - - apiFolder - .Children - .ElementAt(1) + TocDump + .ToText(result.TableOfContents) .Should() - .BeOfType() - .Which - .PathRelativeToDocumentationSet - .Should() - .Be("api/reference.md", "folder path 'api' should be prepended"); + .MatchInlineSnapshot( + """ + FileRef guide.md + FileRef chapter1.md + FileRef section1.md + FolderRef api + IndexFileRef api/index.md + FileRef api/reference.md + """ + ); } [Test] diff --git a/tests/Elastic.Documentation.Configuration.Tests/Elastic.Documentation.Configuration.Tests.csproj b/tests/Elastic.Documentation.Configuration.Tests/Elastic.Documentation.Configuration.Tests.csproj index cac67b170b..86d3f76900 100644 --- a/tests/Elastic.Documentation.Configuration.Tests/Elastic.Documentation.Configuration.Tests.csproj +++ b/tests/Elastic.Documentation.Configuration.Tests/Elastic.Documentation.Configuration.Tests.csproj @@ -9,6 +9,11 @@ + + + + + diff --git a/tests/Elastic.Documentation.Configuration.Tests/SiteNavDump.cs b/tests/Elastic.Documentation.Configuration.Tests/SiteNavDump.cs new file mode 100644 index 0000000000..36a2537ba1 --- /dev/null +++ b/tests/Elastic.Documentation.Configuration.Tests/SiteNavDump.cs @@ -0,0 +1,51 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System.Text; +using Elastic.Documentation.Configuration.Toc; + +namespace Elastic.Documentation.Configuration.Tests; + +/// +/// A deserialized navigation.yml as indented text: the phantoms first, then one line per entry with its +/// source, path prefix and flags, and the children below it. +/// +public static class SiteNavDump +{ + public static string ToText(SiteNavigationFile file) + { + var sb = new StringBuilder(); + foreach (var phantom in file.Phantoms) + sb.Append("phantom ").Append(phantom.Source).Append('\n'); + foreach (var entry in file.TableOfContents) + Write(sb, entry, 0); + return sb.ToString(); + } + + private static void Write(StringBuilder sb, ISiteNavigationEntry entry, int depth) + { + sb.Append(' ', depth * 2); + switch (entry) + { + case SiteTableOfContentsRef toc: + sb.Append("toc ").Append(toc.Source).Append(" path_prefix=\"").Append(toc.PathPrefix).Append('"'); + if (toc.Island) + sb.Append(" [island]"); + if (toc.NavigationTitle is { } title) + sb.Append(" navigation_title=\"").Append(title).Append('"'); + break; + case SiteSectionRef section: + sb.Append("section \"").Append(section.Title).Append('"'); + if (section.ExternalUrl is { } url) + sb.Append(" external=").Append(url); + foreach (var link in section.DropdownLinks) + sb.Append(" dropdown=\"").Append(link.Title).Append("\" ").Append(link.Url); + break; + } + + sb.Append('\n'); + foreach (var child in entry.Children) + Write(sb, child, depth + 1); + } +} diff --git a/tests/Elastic.Documentation.Configuration.Tests/SiteNavigationFileTests.cs b/tests/Elastic.Documentation.Configuration.Tests/SiteNavigationFileTests.cs index dda409193f..59146f960d 100644 --- a/tests/Elastic.Documentation.Configuration.Tests/SiteNavigationFileTests.cs +++ b/tests/Elastic.Documentation.Configuration.Tests/SiteNavigationFileTests.cs @@ -29,25 +29,18 @@ public void DeserializesSiteNavigationFile() var siteNav = SiteNavigationFile.Deserialize(yaml); - siteNav.Should().NotBeNull(); - siteNav.Phantoms.Should().HaveCount(2); - siteNav.Phantoms.ElementAt(0).Source.Should().Be("elasticsearch://reference"); - siteNav.Phantoms.ElementAt(1).Source.Should().Be("docs-content://"); - - siteNav.TableOfContents.Should().HaveCount(3); - - var observability = siteNav.TableOfContents.ElementAt(0).Should().BeOfType().Which; - observability.Source.ToString().Should().Be("docs-content://serverless/observability"); - observability.PathPrefix.Should().Be("/serverless/observability"); - observability.Children.Should().BeEmpty(); - - var search = siteNav.TableOfContents.ElementAt(1).Should().BeOfType().Which; - search.Source.ToString().Should().Be("docs-content://serverless/search"); - search.PathPrefix.Should().Be("/serverless/search"); - - var security = siteNav.TableOfContents.ElementAt(2).Should().BeOfType().Which; - security.Source.ToString().Should().Be("docs-content://serverless/security"); - security.PathPrefix.Should().Be("/serverless/security"); + SiteNavDump + .ToText(siteNav) + .Should() + .MatchInlineSnapshot( + """ + phantom elasticsearch://reference + phantom docs-content:// + toc docs-content://serverless/observability path_prefix="/serverless/observability" + toc docs-content://serverless/search path_prefix="/serverless/search" + toc docs-content://serverless/security path_prefix="/serverless/security" + """ + ); } [Test] @@ -68,20 +61,16 @@ public void DeserializesSiteNavigationFileWithNestedChildren() var siteNav = SiteNavigationFile.Deserialize(yaml); - siteNav.TableOfContents.Should().HaveCount(1); - - var platform = siteNav.TableOfContents.First().Should().BeOfType().Which; - platform.Source.ToString().Should().Be("docs-content://platform/"); - platform.PathPrefix.Should().Be("/platform"); - platform.Children.Should().HaveCount(2); - - var deployment = platform.Children.ElementAt(0); - deployment.Source.ToString().Should().Be("docs-content://platform/deployment-guide"); - deployment.PathPrefix.Should().Be("/platform/deployment"); - - var cloud = platform.Children.ElementAt(1); - cloud.Source.ToString().Should().Be("docs-content://platform/cloud-guide"); - cloud.PathPrefix.Should().Be("/platform/cloud"); + SiteNavDump + .ToText(siteNav) + .Should() + .MatchInlineSnapshot( + """ + toc docs-content://platform/ path_prefix="/platform" + toc docs-content://platform/deployment-guide path_prefix="/platform/deployment" + toc docs-content://platform/cloud-guide path_prefix="/platform/cloud" + """ + ); } [Test] @@ -95,10 +84,7 @@ public void DeserializesWithMissingPath() var siteNav = SiteNavigationFile.Deserialize(yaml); - siteNav.TableOfContents.Should().HaveCount(1); - var ref1 = siteNav.TableOfContents.First().Should().BeOfType().Which; - ref1.Source.ToString().Should().Be("docs-content://elasticsearch/reference"); - ref1.PathPrefix.Should().BeEmpty(); + SiteNavDump.ToText(siteNav).Should().MatchInlineSnapshot("toc docs-content://elasticsearch/reference path_prefix=\"\""); } [Test] @@ -115,16 +101,16 @@ public void PreservesSchemeWhenPresent() var siteNav = SiteNavigationFile.Deserialize(yaml); - siteNav.TableOfContents.Should().HaveCount(3); - - var elasticsearch = siteNav.TableOfContents.ElementAt(0).Should().BeOfType().Which; - elasticsearch.Source.ToString().Should().Be("elasticsearch://reference/current"); - - var kibana = siteNav.TableOfContents.ElementAt(1).Should().BeOfType().Which; - kibana.Source.ToString().Should().Be("kibana://reference/8.0"); - - var serverless = siteNav.TableOfContents.ElementAt(2).Should().BeOfType().Which; - serverless.Source.ToString().Should().Be("docs-content://serverless/observability"); + SiteNavDump + .ToText(siteNav) + .Should() + .MatchInlineSnapshot( + """ + toc elasticsearch://reference/current path_prefix="" + toc kibana://reference/8.0 path_prefix="" + toc docs-content://serverless/observability path_prefix="" + """ + ); } [Test] @@ -143,13 +129,15 @@ public void DeserializesIslandOnTocEntry() var siteNav = SiteNavigationFile.Deserialize(yaml); - siteNav.TableOfContents.Should().HaveCount(2); - - var observability = siteNav.TableOfContents.ElementAt(0).Should().BeOfType().Which; - observability.Island.Should().BeTrue("island: true must be captured on the SiteTableOfContentsRef"); - - var security = siteNav.TableOfContents.ElementAt(1).Should().BeOfType().Which; - security.Island.Should().BeFalse("island defaults to false when omitted"); + SiteNavDump + .ToText(siteNav) + .Should() + .MatchInlineSnapshot( + """ + toc observability:/// path_prefix="observability" [island] + toc security:/// path_prefix="security" + """ + ); } [Test] @@ -202,18 +190,17 @@ public void DeserializesSectionWithChildren() var siteNav = SiteNavigationFile.Deserialize(yaml); - siteNav.TableOfContents.Should().HaveCount(2); - - var guides = siteNav.TableOfContents.ElementAt(0).Should().BeOfType().Which; - guides.Title.Should().Be("Guides"); - guides.IsExternal.Should().BeFalse(); - guides.ExternalUrl.Should().BeNull(); - guides.Children.Should().HaveCount(2); - guides.Children.ElementAt(0).Source.ToString().Should().Be("docs-content://get-started/"); - guides.Children.ElementAt(1).Source.ToString().Should().Be("docs-content://solutions/"); - - var reference = siteNav.TableOfContents.ElementAt(1).Should().BeOfType().Which; - reference.Source.ToString().Should().Be("docs-content://reference/"); + SiteNavDump + .ToText(siteNav) + .Should() + .MatchInlineSnapshot( + """ + section "Guides" + toc docs-content://get-started/ path_prefix="" + toc docs-content://solutions/ path_prefix="" + toc docs-content://reference/ path_prefix="reference" + """ + ); } [Test] @@ -229,13 +216,7 @@ public void DeserializesExternalSection() var siteNav = SiteNavigationFile.Deserialize(yaml); - siteNav.TableOfContents.Should().HaveCount(1); - - var apis = siteNav.TableOfContents.First().Should().BeOfType().Which; - apis.Title.Should().Be("APIs"); - apis.IsExternal.Should().BeTrue(); - apis.ExternalUrl.Should().Be("https://www.elastic.co/docs/api/"); - apis.Children.Should().BeEmpty(); + SiteNavDump.ToText(siteNav).Should().MatchInlineSnapshot("section \"APIs\" external=https://www.elastic.co/docs/api/"); } /// diff --git a/tests/Elastic.Documentation.Configuration.Tests/TocDump.cs b/tests/Elastic.Documentation.Configuration.Tests/TocDump.cs new file mode 100644 index 0000000000..9b19785399 --- /dev/null +++ b/tests/Elastic.Documentation.Configuration.Tests/TocDump.cs @@ -0,0 +1,86 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System.Text; +using Elastic.Documentation.Configuration.Toc; + +namespace Elastic.Documentation.Configuration.Tests; + +/// +/// A table of contents as indented text, one entry per line: Type path followed by the flags that are set +/// ([hidden], [island], title=, sort=, ...). Children are indented below their parent. +/// The refs are records that hold their children behind an interface, which a JSON snapshot would flatten. +/// +public static class TocDump +{ + public static string ToText(IEnumerable items) + { + var sb = new StringBuilder(); + foreach (var item in items) + Write(sb, item, 0); + return sb.ToString(); + } + + private static void Write(StringBuilder sb, ITableOfContentsItem item, int depth) + { + sb.Append(' ', depth * 2).Append(item.GetType().Name).Append(" ").Append(item.PathRelativeToDocumentationSet); + if (item.PathRelativeToContainer != item.PathRelativeToDocumentationSet) + sb.Append(" (container-relative: ").Append(item.PathRelativeToContainer).Append(')'); + + switch (item) + { + case FileRef { Hidden: true }: + case CrossLinkRef { Hidden: true }: + sb.Append(" [hidden]"); + break; + } + + switch (item) + { + case CrossLinkRef { Title: { } title }: + sb.Append(" title=\"").Append(title).Append('"'); + break; + case IsolatedTableOfContentsRef { Island: true }: + sb.Append(" [island]"); + break; + case FolderRef folder: + if (folder.Sort is { } sort) + sb.Append(" sort=").Append(sort); + if (folder.Exclude is { Count: > 0 } exclude) + sb.Append(" exclude=[").Append(string.Join(", ", exclude)).Append(']'); + break; + case ListingRef listing: + sb.Append(" listing=").Append(listing.Options); + break; + case ListingGroupRef group: + sb.Append(" group=").Append(group.GroupKey); + break; + } + + var defaultCta = item switch + { + FileRef f => f.DefaultCta, + FolderRef f => f.DefaultCta, + IsolatedTableOfContentsRef t => t.DefaultCta, + _ => null + }; + if (defaultCta is not null) + sb.Append(" cta=").Append(defaultCta); + + sb.Append('\n'); + + var children = item switch + { + FileRef f => f.Children, + CrossLinkRef c => c.Children, + FolderRef f => f.Children, + IsolatedTableOfContentsRef t => t.Children, + ListingRef l => l.Children, + ListingGroupRef g => g.Children, + _ => [] + }; + foreach (var child in children) + Write(sb, child, depth + 1); + } +} diff --git a/tests/Elastic.Documentation.Configuration.Tests/UnknownYamlKeyTests.cs b/tests/Elastic.Documentation.Configuration.Tests/UnknownYamlKeyTests.cs index 66a7545ca7..b93439e5bf 100644 --- a/tests/Elastic.Documentation.Configuration.Tests/UnknownYamlKeyTests.cs +++ b/tests/Elastic.Documentation.Configuration.Tests/UnknownYamlKeyTests.cs @@ -43,10 +43,12 @@ public void DocumentationSet_TypoInTocItem_ErrorsWithSuggestionInsteadOfDropping new ScopedFileSystem(fileSystem, "/docs") ); - var diagnostic = collector.Captured.Should().ContainSingle(d => d.Severity == Severity.Error).Subject; - diagnostic.Message.Should().Contain("Unknown key 'fiel'").And.Contain("Did you mean 'file'?"); - diagnostic.Line.Should().Be(4); - diagnostic.Column.Should().Be(5); + collector + .Captured + .ToSnapshotText() + .Should() + .MatchInlineSnapshot("error /docs/docset.yml:4 Unknown key 'fiel' in $.toc[1]. Did you mean 'file'?"); + collector.Captured.Should().ContainSingle().Which.Column.Should().Be(5); } [Test] @@ -62,11 +64,11 @@ public void SiteNavigation_TypoInTocEntry_ErrorsWithSuggestion() _ = SiteNavigationFile.Deserialize(yaml, collector, "/config/navigation.yml"); - var diagnostic = collector.Captured.Should().ContainSingle().Subject; - diagnostic.Severity.Should().Be(Severity.Error); - diagnostic.File.Should().Be("/config/navigation.yml"); - diagnostic.Line.Should().Be(3); - diagnostic.Message.Should().Contain("Unknown key 'path_prefx'").And.Contain("Did you mean 'path_prefix'?"); + collector + .Captured + .ToSnapshotText() + .Should() + .MatchInlineSnapshot("error /config/navigation.yml:3 Unknown key 'path_prefx' in $.toc[0]. Did you mean 'path_prefix'?"); } [Test] @@ -84,16 +86,11 @@ public void SiteNavigation_UnknownKeyFarFromAnyKnownKey_ListsValidKeys() collector .Captured + .ToSnapshotText() .Should() - .ContainSingle() - .Which - .Message - .Should() - .Contain("Unknown key 'colour_scheme'") - .And - .Contain("Valid keys:") - .And - .Contain("path_prefix"); + .MatchInlineSnapshot( + "error /config/navigation.yml:3 Unknown key 'colour_scheme' in $.toc[0]. Valid keys: children, dropdown, external, island, navigation_title, path_prefix, section, toc." + ); } [Test] @@ -138,10 +135,13 @@ public void ApiConfiguration_UnknownEntryKey_ReportedThroughCallback() var sequence = YamlSerializer.Deserialize(yaml, options)!; sequence.SingleEntry!.Product.Should().Be("elasticsearch"); - var diagnostic = collector.Captured.Should().ContainSingle().Subject; - diagnostic.Severity.Should().Be(Severity.Error); - diagnostic.Line.Should().Be(3); - diagnostic.Message.Should().Contain("Unknown key 'prodcut_id'"); + collector + .Captured + .ToSnapshotText() + .Should() + .MatchInlineSnapshot( + "error docset.yml:3 Unknown key 'prodcut_id' in $[0]. Valid keys: catalog, children, product, repository, spec." + ); } [Test] @@ -177,17 +177,14 @@ public void LegacyKeyPolicy_CanDowngradeOrSuppressSpecificKeys() collector .Captured - .Select(d => (d.Severity, d.Message)) + .ToSnapshotText() .Should() - .SatisfyRespectively(hint => - { - hint.Severity.Should().Be(Severity.Hint); - hint.Message.Should().Be("'old_key' is deprecated."); - }, error => - { - error.Severity.Should().Be(Severity.Error); - error.Message.Should().Contain("Unknown key 'other'"); - }); + .MatchInlineSnapshot( + """ + hint toc.yml:3 'old_key' is deprecated. + error toc.yml:5 Unknown key 'other' in $[0]. Valid keys: catalog, children, product, repository, spec. + """ + ); } [Test] diff --git a/tests/Navigation.Tests/.snapshots/DesignSystem/DocsBarRenderingTests/AirGapped_UsesTheDesignSystemHeaderInsteadOfTheReactOne.verified.html b/tests/Navigation.Tests/.snapshots/DesignSystem/DocsBarRenderingTests/AirGapped_UsesTheDesignSystemHeaderInsteadOfTheReactOne.verified.html new file mode 100644 index 0000000000..c7f11f3677 --- /dev/null +++ b/tests/Navigation.Tests/.snapshots/DesignSystem/DocsBarRenderingTests/AirGapped_UsesTheDesignSystemHeaderInsteadOfTheReactOne.verified.html @@ -0,0 +1,172 @@ + diff --git a/tests/Navigation.Tests/.snapshots/DesignSystem/DocsBarRenderingTests/DocsBar_RendersTheActiveSectionDropdownExternalSlotSearchAndThemeSwitch.verified.html b/tests/Navigation.Tests/.snapshots/DesignSystem/DocsBarRenderingTests/DocsBar_RendersTheActiveSectionDropdownExternalSlotSearchAndThemeSwitch.verified.html new file mode 100644 index 0000000000..f0654065e2 --- /dev/null +++ b/tests/Navigation.Tests/.snapshots/DesignSystem/DocsBarRenderingTests/DocsBar_RendersTheActiveSectionDropdownExternalSlotSearchAndThemeSwitch.verified.html @@ -0,0 +1,206 @@ +
+
+
+ + +
+ + + + + +
+ +

Type to search.

+
    +
+ +
+ + + ↑ + + ↓ + + to move + + + Enter + + to open + + + Esc + + to close +
+
diff --git a/tests/Navigation.Tests/.snapshots/DesignSystem/NavSideRenderingTests/IslandSidebar_ShowsBackLinksAsPillsAndTheSectionTitleAsAHeading.verified.html b/tests/Navigation.Tests/.snapshots/DesignSystem/NavSideRenderingTests/IslandSidebar_ShowsBackLinksAsPillsAndTheSectionTitleAsAHeading.verified.html new file mode 100644 index 0000000000..b944c53c9d --- /dev/null +++ b/tests/Navigation.Tests/.snapshots/DesignSystem/NavSideRenderingTests/IslandSidebar_ShowsBackLinksAsPillsAndTheSectionTitleAsAHeading.verified.html @@ -0,0 +1,100 @@ + diff --git a/tests/Navigation.Tests/.snapshots/DesignSystem/NavSideRenderingTests/SectionSidebar_WithoutBackLinks_KeepsTheQuietLabel.verified.html b/tests/Navigation.Tests/.snapshots/DesignSystem/NavSideRenderingTests/SectionSidebar_WithoutBackLinks_KeepsTheQuietLabel.verified.html new file mode 100644 index 0000000000..e5458ccff5 --- /dev/null +++ b/tests/Navigation.Tests/.snapshots/DesignSystem/NavSideRenderingTests/SectionSidebar_WithoutBackLinks_KeepsTheQuietLabel.verified.html @@ -0,0 +1,93 @@ + diff --git a/tests/Navigation.Tests/.snapshots/DesignSystem/NavSideRenderingTests/Tree_MarksAbsoluteLinksAsExternal.verified.html b/tests/Navigation.Tests/.snapshots/DesignSystem/NavSideRenderingTests/Tree_MarksAbsoluteLinksAsExternal.verified.html new file mode 100644 index 0000000000..2fce79051b --- /dev/null +++ b/tests/Navigation.Tests/.snapshots/DesignSystem/NavSideRenderingTests/Tree_MarksAbsoluteLinksAsExternal.verified.html @@ -0,0 +1,23 @@ + diff --git a/tests/Navigation.Tests/.snapshots/Rendering/FooterRenderingTests/AssemblerFooter_RendersIubendaLinksAfterTrademarkCopy.verified.html b/tests/Navigation.Tests/.snapshots/Rendering/FooterRenderingTests/AssemblerFooter_RendersIubendaLinksAfterTrademarkCopy.verified.html new file mode 100644 index 0000000000..fe58429c4a --- /dev/null +++ b/tests/Navigation.Tests/.snapshots/Rendering/FooterRenderingTests/AssemblerFooter_RendersIubendaLinksAfterTrademarkCopy.verified.html @@ -0,0 +1,51 @@ +
+
+
+ + Elastic logo + +
+
+
+ +

+ © 2026 Elasticsearch B.V. All Rights Reserved. +

+
+
+

+ This content is available in different formats for convenience only. All original licensing terms apply. +

+

+ Elasticsearch is a trademark of Elasticsearch B.V., registered in the U.S. and in other countries. Apache, Apache Lucene, Apache Hadoop, Hadoop, HDFS and the yellow elephant logo are trademarks of the Apache Software Foundation in the United States and/or other countries. +

+

+ Notice at Collection + | + + Your Privacy Choices + + +

+
+
+
+
diff --git a/tests/Navigation.Tests/.snapshots/Rendering/SecondaryNavRenderingTests/ApiIdentityShowsTheApiAndItsVersionsInOneMenu.verified.html b/tests/Navigation.Tests/.snapshots/Rendering/SecondaryNavRenderingTests/ApiIdentityShowsTheApiAndItsVersionsInOneMenu.verified.html new file mode 100644 index 0000000000..6a06c539ee --- /dev/null +++ b/tests/Navigation.Tests/.snapshots/Rendering/SecondaryNavRenderingTests/ApiIdentityShowsTheApiAndItsVersionsInOneMenu.verified.html @@ -0,0 +1,51 @@ +
+ + +
diff --git a/tests/Navigation.Tests/.snapshots/Rendering/SecondaryNavRenderingTests/ApiIdentityTriggerDropsTheLatestSuffixButTheMenuKeepsIt.verified.html b/tests/Navigation.Tests/.snapshots/Rendering/SecondaryNavRenderingTests/ApiIdentityTriggerDropsTheLatestSuffixButTheMenuKeepsIt.verified.html new file mode 100644 index 0000000000..784405620a --- /dev/null +++ b/tests/Navigation.Tests/.snapshots/Rendering/SecondaryNavRenderingTests/ApiIdentityTriggerDropsTheLatestSuffixButTheMenuKeepsIt.verified.html @@ -0,0 +1,21 @@ +
+ +
+
+
Version
+ main + v9 (latest) + v8 +
+
+
diff --git a/tests/Navigation.Tests/.snapshots/Rendering/SecondaryNavRenderingTests/ApiIdentityWithVersionsOnlyListsTheVersions.verified.html b/tests/Navigation.Tests/.snapshots/Rendering/SecondaryNavRenderingTests/ApiIdentityWithVersionsOnlyListsTheVersions.verified.html new file mode 100644 index 0000000000..887df7e490 --- /dev/null +++ b/tests/Navigation.Tests/.snapshots/Rendering/SecondaryNavRenderingTests/ApiIdentityWithVersionsOnlyListsTheVersions.verified.html @@ -0,0 +1,20 @@ +
+ +
+
+
Version
+ main + v8 +
+
+
diff --git a/tests/Navigation.Tests/.snapshots/Rendering/SecondaryNavRenderingTests/ApiIdentityWithoutVersionsOrOtherApisIsPlainText.verified.html b/tests/Navigation.Tests/.snapshots/Rendering/SecondaryNavRenderingTests/ApiIdentityWithoutVersionsOrOtherApisIsPlainText.verified.html new file mode 100644 index 0000000000..7c03286446 --- /dev/null +++ b/tests/Navigation.Tests/.snapshots/Rendering/SecondaryNavRenderingTests/ApiIdentityWithoutVersionsOrOtherApisIsPlainText.verified.html @@ -0,0 +1,3 @@ +
+ Alpha API +
diff --git a/tests/Navigation.Tests/.snapshots/Rendering/SecondaryNavRenderingTests/ConfiguredTopNavRendersTheDesktopBar.verified.html b/tests/Navigation.Tests/.snapshots/Rendering/SecondaryNavRenderingTests/ConfiguredTopNavRendersTheDesktopBar.verified.html new file mode 100644 index 0000000000..4f811ab4ce --- /dev/null +++ b/tests/Navigation.Tests/.snapshots/Rendering/SecondaryNavRenderingTests/ConfiguredTopNavRendersTheDesktopBar.verified.html @@ -0,0 +1,65 @@ +
+ +
diff --git a/tests/Navigation.Tests/.snapshots/Rendering/SecondaryNavRenderingTests/LinkOnlyTopNavRendersItsLinks.verified.html b/tests/Navigation.Tests/.snapshots/Rendering/SecondaryNavRenderingTests/LinkOnlyTopNavRendersItsLinks.verified.html new file mode 100644 index 0000000000..70ffba471c --- /dev/null +++ b/tests/Navigation.Tests/.snapshots/Rendering/SecondaryNavRenderingTests/LinkOnlyTopNavRendersItsLinks.verified.html @@ -0,0 +1,53 @@ + diff --git a/tests/Navigation.Tests/.snapshots/Rendering/SecondaryNavRenderingTests/MobileDrawerRendersVersionDropdown.verified.html b/tests/Navigation.Tests/.snapshots/Rendering/SecondaryNavRenderingTests/MobileDrawerRendersVersionDropdown.verified.html new file mode 100644 index 0000000000..9dd5920202 --- /dev/null +++ b/tests/Navigation.Tests/.snapshots/Rendering/SecondaryNavRenderingTests/MobileDrawerRendersVersionDropdown.verified.html @@ -0,0 +1,20 @@ + + diff --git a/tests/Navigation.Tests/.snapshots/Rendering/SecondaryNavRenderingTests/TopNavLinksRenderInMobileDrawer.verified.html b/tests/Navigation.Tests/.snapshots/Rendering/SecondaryNavRenderingTests/TopNavLinksRenderInMobileDrawer.verified.html new file mode 100644 index 0000000000..f6f574edde --- /dev/null +++ b/tests/Navigation.Tests/.snapshots/Rendering/SecondaryNavRenderingTests/TopNavLinksRenderInMobileDrawer.verified.html @@ -0,0 +1,34 @@ + + diff --git a/tests/Navigation.Tests/.snapshots/Rendering/SecondaryNavRenderingTests/TopNavMobileDrawerIncludesDocsHome.verified.html b/tests/Navigation.Tests/.snapshots/Rendering/SecondaryNavRenderingTests/TopNavMobileDrawerIncludesDocsHome.verified.html new file mode 100644 index 0000000000..668223b2b5 --- /dev/null +++ b/tests/Navigation.Tests/.snapshots/Rendering/SecondaryNavRenderingTests/TopNavMobileDrawerIncludesDocsHome.verified.html @@ -0,0 +1,34 @@ + + diff --git a/tests/Navigation.Tests/.snapshots/Rendering/SecondaryNavRenderingTests/VersionDropdownDoesNotRenderOnTheLegacyBar.verified.html b/tests/Navigation.Tests/.snapshots/Rendering/SecondaryNavRenderingTests/VersionDropdownDoesNotRenderOnTheLegacyBar.verified.html new file mode 100644 index 0000000000..94c6d23b08 --- /dev/null +++ b/tests/Navigation.Tests/.snapshots/Rendering/SecondaryNavRenderingTests/VersionDropdownDoesNotRenderOnTheLegacyBar.verified.html @@ -0,0 +1,31 @@ +
+ +
diff --git a/tests/Navigation.Tests/.snapshots/Rendering/SecondaryNavRenderingTests/VersionDropdownRendersOnTheBuiltInBar.verified.html b/tests/Navigation.Tests/.snapshots/Rendering/SecondaryNavRenderingTests/VersionDropdownRendersOnTheBuiltInBar.verified.html new file mode 100644 index 0000000000..856771b30e --- /dev/null +++ b/tests/Navigation.Tests/.snapshots/Rendering/SecondaryNavRenderingTests/VersionDropdownRendersOnTheBuiltInBar.verified.html @@ -0,0 +1,50 @@ + diff --git a/tests/Navigation.Tests/.snapshots/Rendering/SecondaryNavRenderingTests/VersionDropdownRendersOnTheRightOfTheTopBar.verified.html b/tests/Navigation.Tests/.snapshots/Rendering/SecondaryNavRenderingTests/VersionDropdownRendersOnTheRightOfTheTopBar.verified.html new file mode 100644 index 0000000000..692b5634e9 --- /dev/null +++ b/tests/Navigation.Tests/.snapshots/Rendering/SecondaryNavRenderingTests/VersionDropdownRendersOnTheRightOfTheTopBar.verified.html @@ -0,0 +1,70 @@ +
+ +
diff --git a/tests/Navigation.Tests/.snapshots/Rendering/SecondaryNavRenderingTests/WithoutConfigurationTheBuiltInLinksAreRendered.verified.html b/tests/Navigation.Tests/.snapshots/Rendering/SecondaryNavRenderingTests/WithoutConfigurationTheBuiltInLinksAreRendered.verified.html new file mode 100644 index 0000000000..1a77a600cf --- /dev/null +++ b/tests/Navigation.Tests/.snapshots/Rendering/SecondaryNavRenderingTests/WithoutConfigurationTheBuiltInLinksAreRendered.verified.html @@ -0,0 +1,45 @@ + diff --git a/tests/Navigation.Tests/.snapshots/Rendering/TableOfContentsRenderingTests/Assembler_FlagOff_RendersVersionDropdown.verified.html b/tests/Navigation.Tests/.snapshots/Rendering/TableOfContentsRenderingTests/Assembler_FlagOff_RendersVersionDropdown.verified.html new file mode 100644 index 0000000000..08a1695813 --- /dev/null +++ b/tests/Navigation.Tests/.snapshots/Rendering/TableOfContentsRenderingTests/Assembler_FlagOff_RendersVersionDropdown.verified.html @@ -0,0 +1,61 @@ + diff --git a/tests/Navigation.Tests/.snapshots/Rendering/TableOfContentsRenderingTests/Assembler_FlagOn_OmitsVersionDropdown.verified.html b/tests/Navigation.Tests/.snapshots/Rendering/TableOfContentsRenderingTests/Assembler_FlagOn_OmitsVersionDropdown.verified.html new file mode 100644 index 0000000000..c15587cfdd --- /dev/null +++ b/tests/Navigation.Tests/.snapshots/Rendering/TableOfContentsRenderingTests/Assembler_FlagOn_OmitsVersionDropdown.verified.html @@ -0,0 +1,57 @@ + diff --git a/tests/Navigation.Tests/.snapshots/Rendering/TableOfContentsRenderingTests/Isolated_OmitsVersionDropdown.verified.html b/tests/Navigation.Tests/.snapshots/Rendering/TableOfContentsRenderingTests/Isolated_OmitsVersionDropdown.verified.html new file mode 100644 index 0000000000..ec8ad9302a --- /dev/null +++ b/tests/Navigation.Tests/.snapshots/Rendering/TableOfContentsRenderingTests/Isolated_OmitsVersionDropdown.verified.html @@ -0,0 +1,22 @@ + diff --git a/tests/Navigation.Tests/.snapshots/Rendering/TocTreeRenderingTests/FolderRow_KeepsThePageLinkAndTheToggleSeparate.verified.html b/tests/Navigation.Tests/.snapshots/Rendering/TocTreeRenderingTests/FolderRow_KeepsThePageLinkAndTheToggleSeparate.verified.html new file mode 100644 index 0000000000..1f7c6845e6 --- /dev/null +++ b/tests/Navigation.Tests/.snapshots/Rendering/TocTreeRenderingTests/FolderRow_KeepsThePageLinkAndTheToggleSeparate.verified.html @@ -0,0 +1,26 @@ + diff --git a/tests/Navigation.Tests/.snapshots/Rendering/TocTreeRenderingTests/HeadingRow_IsALabelNotAPageLink.verified.html b/tests/Navigation.Tests/.snapshots/Rendering/TocTreeRenderingTests/HeadingRow_IsALabelNotAPageLink.verified.html new file mode 100644 index 0000000000..25255b1387 --- /dev/null +++ b/tests/Navigation.Tests/.snapshots/Rendering/TocTreeRenderingTests/HeadingRow_IsALabelNotAPageLink.verified.html @@ -0,0 +1,22 @@ + diff --git a/tests/Navigation.Tests/.snapshots/Rendering/TocTreeRenderingTests/IslandOverview_RendersHeadingAndOverviewLeaf.verified.html b/tests/Navigation.Tests/.snapshots/Rendering/TocTreeRenderingTests/IslandOverview_RendersHeadingAndOverviewLeaf.verified.html new file mode 100644 index 0000000000..ad5e161b12 --- /dev/null +++ b/tests/Navigation.Tests/.snapshots/Rendering/TocTreeRenderingTests/IslandOverview_RendersHeadingAndOverviewLeaf.verified.html @@ -0,0 +1,24 @@ + diff --git a/tests/Navigation.Tests/.snapshots/Rendering/TocTreeRenderingTests/IslandStub_IsALinkWithoutAnExpandControl.verified.html b/tests/Navigation.Tests/.snapshots/Rendering/TocTreeRenderingTests/IslandStub_IsALinkWithoutAnExpandControl.verified.html new file mode 100644 index 0000000000..d4890eeb4a --- /dev/null +++ b/tests/Navigation.Tests/.snapshots/Rendering/TocTreeRenderingTests/IslandStub_IsALinkWithoutAnExpandControl.verified.html @@ -0,0 +1,15 @@ + diff --git a/tests/Navigation.Tests/.snapshots/Rendering/TocTreeRenderingTests/LegacyHeadingRow_RendersTitleWithoutAnEmptyLink.verified.html b/tests/Navigation.Tests/.snapshots/Rendering/TocTreeRenderingTests/LegacyHeadingRow_RendersTitleWithoutAnEmptyLink.verified.html new file mode 100644 index 0000000000..acedab88d9 --- /dev/null +++ b/tests/Navigation.Tests/.snapshots/Rendering/TocTreeRenderingTests/LegacyHeadingRow_RendersTitleWithoutAnEmptyLink.verified.html @@ -0,0 +1,32 @@ +
+
+ +
+ +
diff --git a/tests/Navigation.Tests/.snapshots/Rendering/TocTreeRenderingTests/RootIndex_WithoutTreeSeparator_KeepsAutomaticDivider.verified.html b/tests/Navigation.Tests/.snapshots/Rendering/TocTreeRenderingTests/RootIndex_WithoutTreeSeparator_KeepsAutomaticDivider.verified.html new file mode 100644 index 0000000000..478b01d469 --- /dev/null +++ b/tests/Navigation.Tests/.snapshots/Rendering/TocTreeRenderingTests/RootIndex_WithoutTreeSeparator_KeepsAutomaticDivider.verified.html @@ -0,0 +1,17 @@ + diff --git a/tests/Navigation.Tests/.snapshots/Rendering/TocTreeRenderingTests/TocTree_RendersTheDesignSystemSidebar.verified.html b/tests/Navigation.Tests/.snapshots/Rendering/TocTreeRenderingTests/TocTree_RendersTheDesignSystemSidebar.verified.html new file mode 100644 index 0000000000..ad432044fb --- /dev/null +++ b/tests/Navigation.Tests/.snapshots/Rendering/TocTreeRenderingTests/TocTree_RendersTheDesignSystemSidebar.verified.html @@ -0,0 +1,33 @@ + diff --git a/tests/Navigation.Tests/.snapshots/Rendering/TocTreeRenderingTests/TreeSeparator_RendersAfterIntroLeaves.verified.html b/tests/Navigation.Tests/.snapshots/Rendering/TocTreeRenderingTests/TreeSeparator_RendersAfterIntroLeaves.verified.html new file mode 100644 index 0000000000..6a6888e1f2 --- /dev/null +++ b/tests/Navigation.Tests/.snapshots/Rendering/TocTreeRenderingTests/TreeSeparator_RendersAfterIntroLeaves.verified.html @@ -0,0 +1,32 @@ + diff --git a/tests/Navigation.Tests/.snapshots/Rendering/TocTreeRenderingTests/VersionSwitcher_StaysOutOfTheTree.verified.html b/tests/Navigation.Tests/.snapshots/Rendering/TocTreeRenderingTests/VersionSwitcher_StaysOutOfTheTree.verified.html new file mode 100644 index 0000000000..168274b953 --- /dev/null +++ b/tests/Navigation.Tests/.snapshots/Rendering/TocTreeRenderingTests/VersionSwitcher_StaysOutOfTheTree.verified.html @@ -0,0 +1,11 @@ + diff --git a/tests/Navigation.Tests/Assembler/ComplexSiteNavigationTests.cs b/tests/Navigation.Tests/Assembler/ComplexSiteNavigationTests.cs index 86db964fe2..133bb3802d 100644 --- a/tests/Navigation.Tests/Assembler/ComplexSiteNavigationTests.cs +++ b/tests/Navigation.Tests/Assembler/ComplexSiteNavigationTests.cs @@ -11,6 +11,7 @@ using Elastic.Documentation.Navigation.Isolated; using Elastic.Documentation.Navigation.Isolated.Leaf; using Elastic.Documentation.Navigation.Isolated.Node; +using Elastic.Documentation.Testing; using Microsoft.Extensions.Logging.Abstractions; namespace Elastic.Documentation.Navigation.Tests.Assembler; @@ -124,52 +125,57 @@ public void ComplexNavigationWithMultipleNestedTocsAppliesPathPrefixToRootUrls() var siteNavigation = new SiteNavigation(siteNavFile, siteContext, documentationSets, sitePrefix: null); - // Verify we have all expected top-level items - siteNavigation.NavigationItems.Should().HaveCount(4); - - // Test 1: Observability - verify root URL has path prefix - var observability = siteNavigation.NavigationItems.ElementAt(0) as INodeNavigationItem; - observability.Should().NotBeNull(); - observability.Url.Should().Be("/serverless/observability"); - observability.NavigationTitle.Should().Be(observability.Index.NavigationTitle); - - // Test 2: Serverless Search - verify root URL has path prefix - var search = siteNavigation.NavigationItems.ElementAt(1); - search.Should().NotBeNull(); - search.Url.Should().Be("/serverless/search"); - - // Test 3: Platform - verify root URL has path prefix - var platform = siteNavigation.NavigationItems.ElementAt(2) as INodeNavigationItem; - platform.Should().NotBeNull(); - platform.Url.Should().Be("/platform"); - platform.NavigationItems.Should().HaveCount(2, "platform should only show the two nested TOCs as children"); - - // Verify nested TOC URLs have their specified path prefixes - var deploymentGuide = platform.NavigationItems.ElementAt(0) as INodeNavigationItem; - deploymentGuide.Should().NotBeNull(); - deploymentGuide.Url.Should().Be("/platform/deployment"); - deploymentGuide.NavigationTitle.Should().Be(deploymentGuide.Index.NavigationTitle); - - var cloudGuide = platform.NavigationItems.ElementAt(1); - cloudGuide.Should().NotBeNull(); - cloudGuide.Url.Should().Be("/platform/cloud"); - cloudGuide.NavigationTitle.Should().Be("Cloud Guide"); - - // Test 4: Elasticsearch Reference - verify root URL has path prefix - var elasticsearch = siteNavigation.NavigationItems.ElementAt(3) as INodeNavigationItem; - elasticsearch.Should().NotBeNull(); - elasticsearch.Url.Should().Be("/elasticsearch/reference"); - elasticsearch.NavigationItems.Should().HaveCount(2, "elasticsearch should have read its toc"); - - // rest-apis is a folder (not a TOC) - var restApis = elasticsearch.NavigationItems.ElementAt(0).Should().BeOfType>().Subject; - restApis.Url.Should().Be("/elasticsearch/reference/rest-apis"); - restApis.NavigationItems.Should().HaveCount(2, "rest-apis folder should have 2 files"); - - // Verify the file inside the folder has the correct path prefix - var documentApisFile = restApis.NavigationItems.ElementAt(0).Should().BeOfType>().Subject; - documentApisFile.Url.Should().Be("/elasticsearch/reference/rest-apis/document-apis"); - documentApisFile.NavigationTitle.Should().Be("Document APIs"); + NavigationDump + .ToText(siteNavigation) + .Should() + .MatchInlineSnapshot( + """ + SiteNavigation / "Docs" + index: FileNavigationLeaf /serverless/observability "Serverless Observability" + DocumentationSetNavigation /serverless/observability "Serverless Observability" [island] + index: FileNavigationLeaf /serverless/observability "Serverless Observability" + FolderNavigation /serverless/observability/getting-started/quick-start "Quick Start" + index: FileNavigationLeaf /serverless/observability/getting-started/quick-start "Quick Start" + FileNavigationLeaf /serverless/observability/getting-started/installation "Installation" + FolderNavigation /serverless/observability/monitoring "Monitoring" + index: FileNavigationLeaf /serverless/observability/monitoring "Monitoring" + FileNavigationLeaf /serverless/observability/monitoring/logs "Logs" + FileNavigationLeaf /serverless/observability/monitoring/metrics "Metrics" + FileNavigationLeaf /serverless/observability/monitoring/traces "Traces" + DocumentationSetNavigation /serverless/search "Serverless Search" [island] + index: FileNavigationLeaf /serverless/search "Serverless Search" + FolderNavigation /serverless/search/indexing "Indexing" + index: FileNavigationLeaf /serverless/search/indexing "Indexing" + FileNavigationLeaf /serverless/search/indexing/documents "Documents" + FileNavigationLeaf /serverless/search/indexing/bulk-api "Bulk API" + FolderNavigation /serverless/search/searching "Searching" + index: FileNavigationLeaf /serverless/search/searching "Searching" + FileNavigationLeaf /serverless/search/searching/query-dsl "Query DSL" + DocumentationSetNavigation /platform "Platform" [island] + index: FileNavigationLeaf /platform "Platform" + TableOfContentsNavigation /platform/deployment "Deployment Guide" + index: FileNavigationLeaf /platform/deployment "Deployment Guide" + FolderNavigation /platform/deployment/self-managed/installation "Installation" + index: FileNavigationLeaf /platform/deployment/self-managed/installation "Installation" + FileNavigationLeaf /platform/deployment/self-managed/configuration "Configuration" + TableOfContentsNavigation /platform/cloud "Cloud Guide" + index: FileNavigationLeaf /platform/cloud "Cloud Guide" + FolderNavigation /platform/cloud/aws/setup "AWS Setup" + index: FileNavigationLeaf /platform/cloud/aws/setup "AWS Setup" + FolderNavigation /platform/cloud/azure/setup "Azure Setup" + index: FileNavigationLeaf /platform/cloud/azure/setup "Azure Setup" + DocumentationSetNavigation /elasticsearch/reference "Elasticsearch Reference" [island] + index: FileNavigationLeaf /elasticsearch/reference "Elasticsearch Reference" + FolderNavigation /elasticsearch/reference/rest-apis "REST APIs" + index: FileNavigationLeaf /elasticsearch/reference/rest-apis "REST APIs" + FileNavigationLeaf /elasticsearch/reference/rest-apis/document-apis "Document APIs" + FileNavigationLeaf /elasticsearch/reference/rest-apis/search-apis "Search APIs" + FolderNavigation /elasticsearch/reference/query-dsl "Query DSL" + index: FileNavigationLeaf /elasticsearch/reference/query-dsl "Query DSL" + FileNavigationLeaf /elasticsearch/reference/query-dsl/term-queries "Term Queries" + FileNavigationLeaf /elasticsearch/reference/query-dsl/full-text-queries "Full Text Queries" + """ + ); } [Test] @@ -207,23 +213,28 @@ public void DeeplyNestedNavigationMaintainsPathPrefixThroughoutHierarchy() var siteNavigation = new SiteNavigation(siteNavFile, siteContext, documentationSets, sitePrefix: null); - var platform = siteNavigation.NavigationItems.First() as INodeNavigationItem; - platform.Should().NotBeNull(); - platform.Url.Should().Be("/docs/platform"); - - // Platform should have its children (deployment-guide, cloud-guide) - platform.NavigationItems.Should().HaveCount(2); - - // Find the deployment-guide TOC (it's the second item after index) - var deploymentGuide = platform.NavigationItems.ElementAt(0) as INodeNavigationItem; - deploymentGuide.Should().NotBeNull(); - deploymentGuide.Should().BeOfType>(); - deploymentGuide.Url.Should().StartWith("/docs/platform"); - - // Walk through the entire tree and verify every single URL starts with a path prefix - var allUrls = CollectAllUrls(platform.NavigationItems); - allUrls.Should().NotBeEmpty(); - allUrls.Should().OnlyContain(url => url.StartsWith("/docs/platform/"), "all URLs in platform should start with /docs/platform"); + NavigationDump + .ToText(siteNavigation) + .Should() + .MatchInlineSnapshot( + """ + SiteNavigation / "Docs" + index: FileNavigationLeaf /docs/platform "Platform" + DocumentationSetNavigation /docs/platform "Platform" [island] + index: FileNavigationLeaf /docs/platform "Platform" + TableOfContentsNavigation /docs/platform/deployment-guide "Deployment Guide" + index: FileNavigationLeaf /docs/platform/deployment-guide "Deployment Guide" + FolderNavigation /docs/platform/deployment-guide/self-managed/installation "Installation" + index: FileNavigationLeaf /docs/platform/deployment-guide/self-managed/installation "Installation" + FileNavigationLeaf /docs/platform/deployment-guide/self-managed/configuration "Configuration" + TableOfContentsNavigation /docs/platform/cloud-guide "Cloud Guide" + index: FileNavigationLeaf /docs/platform/cloud-guide "Cloud Guide" + FolderNavigation /docs/platform/cloud-guide/aws/setup "AWS Setup" + index: FileNavigationLeaf /docs/platform/cloud-guide/aws/setup "AWS Setup" + FolderNavigation /docs/platform/cloud-guide/azure/setup "Azure Setup" + index: FileNavigationLeaf /docs/platform/cloud-guide/azure/setup "Azure Setup" + """ + ); } [Test] @@ -261,39 +272,28 @@ public void FileNavigationLeafUrlsReflectPathPrefixInDeeplyNestedStructures() var siteNavigation = new SiteNavigation(siteNavFile, siteContext, documentationSets, sitePrefix: null); - var platform = siteNavigation.NavigationItems.First() as INodeNavigationItem; - platform.Should().NotBeNull(); - - // Platform should have its children including deployment-guide TOC - platform.NavigationItems.Should().HaveCount(2); - - // Get deployment-guide TOC - var deploymentGuide = platform.NavigationItems.ElementAt(0) as INodeNavigationItem; - deploymentGuide.Should().NotBeNull(); - deploymentGuide.Should().BeOfType>(); - - // Find all FileNavigationLeaf items recursively - var fileLeaves = CollectAllFileLeaves(deploymentGuide.NavigationItems); - fileLeaves.Should().NotBeEmpty("deployment-guide should contain file leaves"); - - // Verify every single file leaf has the correct path prefix - foreach (var fileLeaf in fileLeaves) - { - fileLeaf - .Url - .Should() - .StartWith( - "/platform", - $"file '{fileLeaf.NavigationTitle}' should have URL starting with /platform but got '{fileLeaf.Url}'" - ); - } - - // Verify at least one specific file to ensure we're testing real data - var indexFile = fileLeaves.OfType>().FirstOrDefault( - f => f.FileInfo.FullName.EndsWith(".md", StringComparison.OrdinalIgnoreCase) - ); - indexFile.Should().NotBeNull(); - indexFile.Url.Should().StartWith("/platform"); + NavigationDump + .ToText(siteNavigation) + .Should() + .MatchInlineSnapshot( + """ + SiteNavigation / "Docs" + index: FileNavigationLeaf /platform "Platform" + DocumentationSetNavigation /platform "Platform" [island] + index: FileNavigationLeaf /platform "Platform" + TableOfContentsNavigation /platform/deployment-guide "Deployment Guide" + index: FileNavigationLeaf /platform/deployment-guide "Deployment Guide" + FolderNavigation /platform/deployment-guide/self-managed/installation "Installation" + index: FileNavigationLeaf /platform/deployment-guide/self-managed/installation "Installation" + FileNavigationLeaf /platform/deployment-guide/self-managed/configuration "Configuration" + TableOfContentsNavigation /platform/cloud-guide "Cloud Guide" + index: FileNavigationLeaf /platform/cloud-guide "Cloud Guide" + FolderNavigation /platform/cloud-guide/aws/setup "AWS Setup" + index: FileNavigationLeaf /platform/cloud-guide/aws/setup "AWS Setup" + FolderNavigation /platform/cloud-guide/azure/setup "Azure Setup" + index: FileNavigationLeaf /platform/cloud-guide/azure/setup "Azure Setup" + """ + ); } [Test] @@ -332,107 +332,27 @@ public void FolderNavigationWithinNestedTocsHasCorrectPathPrefix() var siteNavigation = new SiteNavigation(siteNavFile, siteContext, documentationSets, sitePrefix: null); - var platform = siteNavigation.NavigationItems.First() as INodeNavigationItem; - platform.Should().NotBeNull(); - - // Platform should have its children including cloud-guide TOC - platform.NavigationItems.Should().HaveCount(2); - - // Get cloud-guide TOC (third item after deployment-guide) - var cloudGuide = platform.NavigationItems.ElementAt(1) as INodeNavigationItem; - cloudGuide.Should().NotBeNull(); - cloudGuide.Should().BeOfType>(); - - // cloud-guide should have folders (index, aws, azure) - var folders = cloudGuide.NavigationItems.OfType>().ToList(); - - folders.Should().NotBeEmpty("cloud-guide should contain folders"); - - // Verify each folder and all its contents have a correct path prefix - foreach (var folder in folders) - { - folder - .Url - .Should() - .StartWith("/platform/cloud", $"folder '{folder.NavigationTitle}' should have URL starting with /platform/cloud"); - - // Verify all items within the folder - AssertAllUrlsStartWith(folder.NavigationItems, "/platform/cloud"); - - // Verify specific file leaves within the folder - var filesInFolder = CollectAllFileLeaves(folder.NavigationItems); - foreach (var file in filesInFolder) - { - file - .Url - .Should() - .StartWith( - "/platform/cloud", - $"file '{file.NavigationTitle}' in folder '{folder.NavigationTitle}' should have URL starting with /platform/cloud" - ); - } - } - } - - /// - /// Helper method to recursively assert all URLs start with a given prefix - /// - private static void AssertAllUrlsStartWith(IEnumerable items, string expectedPrefix) - { - foreach (var item in items) - { - item - .Url - .Should() - .StartWith( - expectedPrefix, - $"item '{item.NavigationTitle}' should have URL starting with '{expectedPrefix}' but got '{item.Url}'" - ); - - if (item is INodeNavigationItem nodeItem) - AssertAllUrlsStartWith(nodeItem.NavigationItems, expectedPrefix); - } - } - - /// - /// Helper method to collect all URLs recursively - /// - private static List CollectAllUrls(IEnumerable items) - { - var urls = new List(); - - foreach (var item in items) - { - urls.Add(item.Url); - - if (item is INodeNavigationItem nodeItem) - urls.AddRange(CollectAllUrls(nodeItem.NavigationItems)); - } - - return urls; - } - - /// - /// Helper method to collect all FileNavigationLeaf items recursively - /// - private static List> CollectAllFileLeaves(IEnumerable items) - { - var fileLeaves = new List>(); - - foreach (var item in items) - { - switch (item) - { - case ILeafNavigationItem fileLeaf: - fileLeaves.Add(fileLeaf); - break; - case INodeNavigationItem node: - fileLeaves.Add(node.Index); - fileLeaves.AddRange(CollectAllFileLeaves(node.NavigationItems)); - break; - } - } - - return fileLeaves; + NavigationDump + .ToText(siteNavigation) + .Should() + .MatchInlineSnapshot( + """ + SiteNavigation / "Docs" + index: FileNavigationLeaf /platform/cloud "Platform" + DocumentationSetNavigation /platform/cloud "Platform" [island] + index: FileNavigationLeaf /platform/cloud "Platform" + TableOfContentsNavigation /platform/cloud "Deployment Guide" + index: FileNavigationLeaf /platform/cloud "Deployment Guide" + FolderNavigation /platform/cloud/self-managed/installation "Installation" + index: FileNavigationLeaf /platform/cloud/self-managed/installation "Installation" + FileNavigationLeaf /platform/cloud/self-managed/configuration "Configuration" + TableOfContentsNavigation /platform/cloud "Cloud Guide" + index: FileNavigationLeaf /platform/cloud "Cloud Guide" + FolderNavigation /platform/cloud/aws/setup "AWS Setup" + index: FileNavigationLeaf /platform/cloud/aws/setup "AWS Setup" + FolderNavigation /platform/cloud/azure/setup "Azure Setup" + index: FileNavigationLeaf /platform/cloud/azure/setup "Azure Setup" + """ + ); } } diff --git a/tests/Navigation.Tests/Assembler/SectionNavigationTests.cs b/tests/Navigation.Tests/Assembler/SectionNavigationTests.cs index 4034eb7e1b..5a060c5c99 100644 --- a/tests/Navigation.Tests/Assembler/SectionNavigationTests.cs +++ b/tests/Navigation.Tests/Assembler/SectionNavigationTests.cs @@ -10,6 +10,7 @@ using Elastic.Documentation.Navigation.Assembler; using Elastic.Documentation.Navigation.Isolated.Node; using Elastic.Documentation.Site.Navigation; +using Elastic.Documentation.Testing; namespace Elastic.Documentation.Navigation.Tests.Assembler; @@ -77,11 +78,36 @@ public void SectionWithChildren_CreatesSectionNavigationNode() var (nav, _, _) = BuildTwoChildSection(yaml); // Top-level should have exactly one item: the SectionNavigation - nav.NavigationItems.Should().HaveCount(1); - var section = nav.NavigationItems.First().Should().BeOfType().Subject; - - section.Title.Should().Be("Guides"); - section.NavigationItems.Should().HaveCount(2); + NavigationDump + .ToText(nav) + .Should() + .MatchInlineSnapshot( + """ + SiteNavigation /docs "Docs" + index: SectionIndexLeaf /docs/observability "Guides" + SectionNavigation /docs/observability "Guides" [island] + index: SectionIndexLeaf /docs/observability "Guides" + DocumentationSetNavigation /docs/observability "Serverless Observability" + index: FileNavigationLeaf /docs/observability "Serverless Observability" + FolderNavigation /docs/observability/getting-started/quick-start "Quick Start" + index: FileNavigationLeaf /docs/observability/getting-started/quick-start "Quick Start" + FileNavigationLeaf /docs/observability/getting-started/installation "Installation" + FolderNavigation /docs/observability/monitoring "Monitoring" + index: FileNavigationLeaf /docs/observability/monitoring "Monitoring" + FileNavigationLeaf /docs/observability/monitoring/logs "Logs" + FileNavigationLeaf /docs/observability/monitoring/metrics "Metrics" + FileNavigationLeaf /docs/observability/monitoring/traces "Traces" + DocumentationSetNavigation /docs/search "Serverless Search" + index: FileNavigationLeaf /docs/search "Serverless Search" + FolderNavigation /docs/search/indexing "Indexing" + index: FileNavigationLeaf /docs/search/indexing "Indexing" + FileNavigationLeaf /docs/search/indexing/documents "Documents" + FileNavigationLeaf /docs/search/indexing/bulk-api "Bulk API" + FolderNavigation /docs/search/searching "Searching" + index: FileNavigationLeaf /docs/search/searching "Searching" + FileNavigationLeaf /docs/search/searching/query-dsl "Query DSL" + """ + ); } [Test] diff --git a/tests/Navigation.Tests/Assembler/SiteDocumentationSetsTests.cs b/tests/Navigation.Tests/Assembler/SiteDocumentationSetsTests.cs index f385d73f18..3735dbe97b 100644 --- a/tests/Navigation.Tests/Assembler/SiteDocumentationSetsTests.cs +++ b/tests/Navigation.Tests/Assembler/SiteDocumentationSetsTests.cs @@ -10,6 +10,7 @@ using Elastic.Documentation.Navigation.Isolated; using Elastic.Documentation.Navigation.Isolated.Leaf; using Elastic.Documentation.Navigation.Isolated.Node; +using Elastic.Documentation.Testing; namespace Elastic.Documentation.Navigation.Tests.Assembler; @@ -124,18 +125,43 @@ public void SiteNavigationIntegratesWithDocumentationSets() var siteNavigation = new SiteNavigation(siteNavFile, siteContext, documentationSets, sitePrefix: null); - siteNavigation.Should().NotBeNull(); - siteNavigation.NavigationItems.Should().HaveCount(3); - - var observability = siteNavigation.NavigationItems.ElementAt(0); - observability.Url.Should().Be("/serverless/observability"); - observability.NavigationTitle.Should().NotBeNullOrEmpty(); - - var search = siteNavigation.NavigationItems.ElementAt(1); - search.Url.Should().Be("/serverless/search"); - - var security = siteNavigation.NavigationItems.ElementAt(2); - security.Url.Should().Be("/serverless/security"); + NavigationDump + .ToText(siteNavigation) + .Should() + .MatchInlineSnapshot( + """ + SiteNavigation / "Docs" + index: FileNavigationLeaf /serverless/observability "Serverless Observability" + DocumentationSetNavigation /serverless/observability "Serverless Observability" [island] + index: FileNavigationLeaf /serverless/observability "Serverless Observability" + FolderNavigation /serverless/observability/getting-started/quick-start "Quick Start" + index: FileNavigationLeaf /serverless/observability/getting-started/quick-start "Quick Start" + FileNavigationLeaf /serverless/observability/getting-started/installation "Installation" + FolderNavigation /serverless/observability/monitoring "Monitoring" + index: FileNavigationLeaf /serverless/observability/monitoring "Monitoring" + FileNavigationLeaf /serverless/observability/monitoring/logs "Logs" + FileNavigationLeaf /serverless/observability/monitoring/metrics "Metrics" + FileNavigationLeaf /serverless/observability/monitoring/traces "Traces" + DocumentationSetNavigation /serverless/search "Serverless Search" [island] + index: FileNavigationLeaf /serverless/search "Serverless Search" + FolderNavigation /serverless/search/indexing "Indexing" + index: FileNavigationLeaf /serverless/search/indexing "Indexing" + FileNavigationLeaf /serverless/search/indexing/documents "Documents" + FileNavigationLeaf /serverless/search/indexing/bulk-api "Bulk API" + FolderNavigation /serverless/search/searching "Searching" + index: FileNavigationLeaf /serverless/search/searching "Searching" + FileNavigationLeaf /serverless/search/searching/query-dsl "Query DSL" + DocumentationSetNavigation /serverless/security "Serverless Security" [island] + index: FileNavigationLeaf /serverless/security "Serverless Security" + FolderNavigation /serverless/security/authentication "Authentication" + index: FileNavigationLeaf /serverless/security/authentication "Authentication" + FileNavigationLeaf /serverless/security/authentication/api-keys "API Keys" + FileNavigationLeaf /serverless/security/authentication/oauth "OAuth" + FolderNavigation /serverless/security/authorization "Authorization" + index: FileNavigationLeaf /serverless/security/authorization "Authorization" + FileNavigationLeaf /serverless/security/authorization/rbac "RBAC" + """ + ); } [Test] @@ -180,18 +206,28 @@ public void SiteNavigationWithNestedTocs() var siteNavigation = new SiteNavigation(siteNavFile, siteContext, documentationSets, sitePrefix: null); - siteNavigation.NavigationItems.Should().HaveCount(1); - - var platform = siteNavigation.NavigationItems.First() as INodeNavigationItem; - platform.Should().NotBeNull(); - platform.Url.Should().Be("/platform"); - platform.NavigationItems.Should().HaveCount(2); - - var deployment = platform.NavigationItems.ElementAt(0); - deployment.Url.Should().Be("/platform/deployment"); - - var cloud = platform.NavigationItems.ElementAt(1); - cloud.Url.Should().Be("/platform/cloud"); + NavigationDump + .ToText(siteNavigation) + .Should() + .MatchInlineSnapshot( + """ + SiteNavigation / "Docs" + index: FileNavigationLeaf /platform "Platform" + DocumentationSetNavigation /platform "Platform" [island] + index: FileNavigationLeaf /platform "Platform" + TableOfContentsNavigation /platform/deployment "Deployment Guide" + index: FileNavigationLeaf /platform/deployment "Deployment Guide" + FolderNavigation /platform/deployment/self-managed/installation "Installation" + index: FileNavigationLeaf /platform/deployment/self-managed/installation "Installation" + FileNavigationLeaf /platform/deployment/self-managed/configuration "Configuration" + TableOfContentsNavigation /platform/cloud "Cloud Guide" + index: FileNavigationLeaf /platform/cloud "Cloud Guide" + FolderNavigation /platform/cloud/aws/setup "AWS Setup" + index: FileNavigationLeaf /platform/cloud/aws/setup "AWS Setup" + FolderNavigation /platform/cloud/azure/setup "Azure Setup" + index: FileNavigationLeaf /platform/cloud/azure/setup "Azure Setup" + """ + ); } [Test] @@ -249,26 +285,66 @@ public void SiteNavigationWithAllRepositories() var siteNavigation = new SiteNavigation(siteNavFile, siteContext, documentationSets, sitePrefix: null); - siteNavigation.Should().NotBeNull(); - siteNavigation.NavigationItems.Should().HaveCount(5); - - // Verify top-level items - var observability = siteNavigation.NavigationItems.ElementAt(0); - observability.Url.Should().Be("/serverless/observability"); - - var search = siteNavigation.NavigationItems.ElementAt(1); - search.Url.Should().Be("/serverless/search"); - - var security = siteNavigation.NavigationItems.ElementAt(2); - security.Url.Should().Be("/serverless/security"); - - var platform = siteNavigation.NavigationItems.ElementAt(3) as INodeNavigationItem; - platform.Should().NotBeNull(); - platform.Url.Should().Be("/platform"); - platform.NavigationItems.Should().HaveCount(2); - - var elasticsearch = siteNavigation.NavigationItems.ElementAt(4); - elasticsearch.Url.Should().Be("/elasticsearch/reference"); + NavigationDump + .ToText(siteNavigation) + .Should() + .MatchInlineSnapshot( + """ + SiteNavigation / "Docs" + index: FileNavigationLeaf /serverless/observability "Serverless Observability" + DocumentationSetNavigation /serverless/observability "Serverless Observability" [island] + index: FileNavigationLeaf /serverless/observability "Serverless Observability" + FolderNavigation /serverless/observability/getting-started/quick-start "Quick Start" + index: FileNavigationLeaf /serverless/observability/getting-started/quick-start "Quick Start" + FileNavigationLeaf /serverless/observability/getting-started/installation "Installation" + FolderNavigation /serverless/observability/monitoring "Monitoring" + index: FileNavigationLeaf /serverless/observability/monitoring "Monitoring" + FileNavigationLeaf /serverless/observability/monitoring/logs "Logs" + FileNavigationLeaf /serverless/observability/monitoring/metrics "Metrics" + FileNavigationLeaf /serverless/observability/monitoring/traces "Traces" + DocumentationSetNavigation /serverless/search "Serverless Search" [island] + index: FileNavigationLeaf /serverless/search "Serverless Search" + FolderNavigation /serverless/search/indexing "Indexing" + index: FileNavigationLeaf /serverless/search/indexing "Indexing" + FileNavigationLeaf /serverless/search/indexing/documents "Documents" + FileNavigationLeaf /serverless/search/indexing/bulk-api "Bulk API" + FolderNavigation /serverless/search/searching "Searching" + index: FileNavigationLeaf /serverless/search/searching "Searching" + FileNavigationLeaf /serverless/search/searching/query-dsl "Query DSL" + DocumentationSetNavigation /serverless/security "Serverless Security" [island] + index: FileNavigationLeaf /serverless/security "Serverless Security" + FolderNavigation /serverless/security/authentication "Authentication" + index: FileNavigationLeaf /serverless/security/authentication "Authentication" + FileNavigationLeaf /serverless/security/authentication/api-keys "API Keys" + FileNavigationLeaf /serverless/security/authentication/oauth "OAuth" + FolderNavigation /serverless/security/authorization "Authorization" + index: FileNavigationLeaf /serverless/security/authorization "Authorization" + FileNavigationLeaf /serverless/security/authorization/rbac "RBAC" + DocumentationSetNavigation /platform "Platform" [island] + index: FileNavigationLeaf /platform "Platform" + TableOfContentsNavigation /platform "Deployment Guide" + index: FileNavigationLeaf /platform "Deployment Guide" + FolderNavigation /platform/self-managed/installation "Installation" + index: FileNavigationLeaf /platform/self-managed/installation "Installation" + FileNavigationLeaf /platform/self-managed/configuration "Configuration" + TableOfContentsNavigation /platform "Cloud Guide" + index: FileNavigationLeaf /platform "Cloud Guide" + FolderNavigation /platform/aws/setup "AWS Setup" + index: FileNavigationLeaf /platform/aws/setup "AWS Setup" + FolderNavigation /platform/azure/setup "Azure Setup" + index: FileNavigationLeaf /platform/azure/setup "Azure Setup" + DocumentationSetNavigation /elasticsearch/reference "Elasticsearch Reference" [island] + index: FileNavigationLeaf /elasticsearch/reference "Elasticsearch Reference" + FolderNavigation /elasticsearch/reference/rest-apis "REST APIs" + index: FileNavigationLeaf /elasticsearch/reference/rest-apis "REST APIs" + FileNavigationLeaf /elasticsearch/reference/rest-apis/document-apis "Document APIs" + FileNavigationLeaf /elasticsearch/reference/rest-apis/search-apis "Search APIs" + FolderNavigation /elasticsearch/reference/query-dsl "Query DSL" + index: FileNavigationLeaf /elasticsearch/reference/query-dsl "Query DSL" + FileNavigationLeaf /elasticsearch/reference/query-dsl/term-queries "Term Queries" + FileNavigationLeaf /elasticsearch/reference/query-dsl/full-text-queries "Full Text Queries" + """ + ); } [Test] @@ -289,23 +365,23 @@ public void DocumentationSetNavigationHasCorrectStructure() GenericDocumentationFileFactory.Instance ); - observabilityNav.NavigationTitle.Should().Be(observabilityNav.NavigationTitle); - observabilityNav.NavigationItems.Should().HaveCount(2); // index.md, getting-started folder, monitoring folder - - var indexFile = observabilityNav.Index; - indexFile.Should().BeOfType>(); - indexFile.Url.Should().Be("/"); - - var gettingStarted = observabilityNav.NavigationItems.ElementAt(0); - gettingStarted.Should().BeOfType>(); - var gettingStartedFolder = (FolderNavigation)gettingStarted; - gettingStartedFolder.Index.Should().NotBeNull(); //quick-start.md - gettingStartedFolder.NavigationItems.Should().HaveCount(1); // installation.md - - var monitoring = observabilityNav.NavigationItems.ElementAt(1); - monitoring.Should().BeOfType>(); - var monitoringFolder = (FolderNavigation)monitoring; - monitoringFolder.NavigationItems.Should().HaveCount(3); // logs.md, metrics.md, traces.md + NavigationDump + .ToText(observabilityNav) + .Should() + .MatchInlineSnapshot( + """ + DocumentationSetNavigation / "Serverless Observability" + index: FileNavigationLeaf / "Serverless Observability" + FolderNavigation /getting-started/quick-start "Quick Start" + index: FileNavigationLeaf /getting-started/quick-start "Quick Start" + FileNavigationLeaf /getting-started/installation "Installation" + FolderNavigation /monitoring "Monitoring" + index: FileNavigationLeaf /monitoring "Monitoring" + FileNavigationLeaf /monitoring/logs "Logs" + FileNavigationLeaf /monitoring/metrics "Metrics" + FileNavigationLeaf /monitoring/traces "Traces" + """ + ); } [Test] @@ -326,24 +402,26 @@ public void DocumentationSetWithNestedTocs() GenericDocumentationFileFactory.Instance ); - platformNav.NavigationTitle.Should().Be("Platform"); - platformNav.NavigationItems.Should().HaveCount(2); // deployment-guide TOC, cloud-guide TOC - - var indexFile = platformNav.Index; - indexFile.Should().BeOfType>(); - indexFile.Url.Should().Be("/"); - - var deploymentGuide = platformNav.NavigationItems.ElementAt(0); - deploymentGuide.Should().BeOfType>(); - deploymentGuide.Url.Should().Be("/deployment-guide"); - var deploymentToc = (TableOfContentsNavigation)deploymentGuide; - deploymentToc.NavigationItems.Should().HaveCount(1); // self-managed folder - - var cloudGuide = platformNav.NavigationItems.ElementAt(1); - cloudGuide.Should().BeOfType>(); - cloudGuide.Url.Should().Be("/cloud-guide"); - var cloudToc = (TableOfContentsNavigation)cloudGuide; - cloudToc.NavigationItems.Should().HaveCount(2); // aws folder, azure folder + NavigationDump + .ToText(platformNav) + .Should() + .MatchInlineSnapshot( + """ + DocumentationSetNavigation / "Platform" + index: FileNavigationLeaf / "Platform" + TableOfContentsNavigation /deployment-guide "Deployment Guide" + index: FileNavigationLeaf /deployment-guide "Deployment Guide" + FolderNavigation /deployment-guide/self-managed/installation "Installation" + index: FileNavigationLeaf /deployment-guide/self-managed/installation "Installation" + FileNavigationLeaf /deployment-guide/self-managed/configuration "Configuration" + TableOfContentsNavigation /cloud-guide "Cloud Guide" + index: FileNavigationLeaf /cloud-guide "Cloud Guide" + FolderNavigation /cloud-guide/aws/setup "AWS Setup" + index: FileNavigationLeaf /cloud-guide/aws/setup "AWS Setup" + FolderNavigation /cloud-guide/azure/setup "Azure Setup" + index: FileNavigationLeaf /cloud-guide/azure/setup "Azure Setup" + """ + ); } [Test] @@ -364,18 +442,22 @@ public void DocumentationSetWithUnderscoreDocset() GenericDocumentationFileFactory.Instance ); - securityNav.NavigationTitle.Should().Be("Serverless Security"); - securityNav.NavigationItems.Should().HaveCount(2); // authentication folder, authorization folder - - var authentication = securityNav.NavigationItems.ElementAt(0); - authentication.Should().BeOfType>(); - var authenticationFolder = (FolderNavigation)authentication; - authenticationFolder.NavigationItems.Should().HaveCount(2); // api-keys.md, oauth.md - - var authorization = securityNav.NavigationItems.ElementAt(1); - authorization.Should().BeOfType>(); - var authorizationFolder = (FolderNavigation)authorization; - authorizationFolder.NavigationItems.Should().HaveCount(1); // rbac.md + NavigationDump + .ToText(securityNav) + .Should() + .MatchInlineSnapshot( + """ + DocumentationSetNavigation / "Serverless Security" + index: FileNavigationLeaf / "Serverless Security" + FolderNavigation /authentication "Authentication" + index: FileNavigationLeaf /authentication "Authentication" + FileNavigationLeaf /authentication/api-keys "API Keys" + FileNavigationLeaf /authentication/oauth "OAuth" + FolderNavigation /authorization "Authorization" + index: FileNavigationLeaf /authorization "Authorization" + FileNavigationLeaf /authorization/rbac "RBAC" + """ + ); } [Test] @@ -411,15 +493,25 @@ public void SiteNavigationAppliesPathPrefixToAllUrls() var siteNavigation = new SiteNavigation(siteNavFile, siteContext, documentationSets, sitePrefix: null); // Verify root URL has path prefix - var root = siteNavigation.NavigationItems.First(); - root.Url.Should().StartWith("/serverless/observability"); - - // Verify all nested items also have the path prefix - if (root is INodeNavigationItem nodeItem) - { - foreach (var item in nodeItem.NavigationItems) - item.Url.Should().StartWith("/serverless/observability"); - } + NavigationDump + .ToText(siteNavigation) + .Should() + .MatchInlineSnapshot( + """ + SiteNavigation / "Docs" + index: FileNavigationLeaf /serverless/observability "Serverless Observability" + DocumentationSetNavigation /serverless/observability "Serverless Observability" [island] + index: FileNavigationLeaf /serverless/observability "Serverless Observability" + FolderNavigation /serverless/observability/getting-started/quick-start "Quick Start" + index: FileNavigationLeaf /serverless/observability/getting-started/quick-start "Quick Start" + FileNavigationLeaf /serverless/observability/getting-started/installation "Installation" + FolderNavigation /serverless/observability/monitoring "Monitoring" + index: FileNavigationLeaf /serverless/observability/monitoring "Monitoring" + FileNavigationLeaf /serverless/observability/monitoring/logs "Logs" + FileNavigationLeaf /serverless/observability/monitoring/metrics "Metrics" + FileNavigationLeaf /serverless/observability/monitoring/traces "Traces" + """ + ); } [Test] @@ -455,16 +547,28 @@ public void SiteNavigationWithNestedTocsAppliesCorrectPathPrefixes() var siteContext = SiteNavigationTestFixture.CreateAssemblerContext(fileSystem, "/checkouts/current/platform"); var siteNavigation = new SiteNavigation(siteNavFile, siteContext, documentationSets, sitePrefix: null); - var platform = siteNavigation.NavigationItems.First() as INodeNavigationItem; - platform.Should().NotBeNull(); - platform.Url.Should().Be("/platform"); - - // Verify child TOCs have their specific path prefixes - var deployment = platform.NavigationItems.ElementAt(0); - deployment.Url.Should().StartWith("/platform/deployment"); - - var cloud = platform.NavigationItems.ElementAt(1); - cloud.Url.Should().StartWith("/platform/cloud"); + NavigationDump + .ToText(siteNavigation) + .Should() + .MatchInlineSnapshot( + """ + SiteNavigation / "Docs" + index: FileNavigationLeaf /platform "Platform" + DocumentationSetNavigation /platform "Platform" [island] + index: FileNavigationLeaf /platform "Platform" + TableOfContentsNavigation /platform/deployment "Deployment Guide" + index: FileNavigationLeaf /platform/deployment "Deployment Guide" + FolderNavigation /platform/deployment/self-managed/installation "Installation" + index: FileNavigationLeaf /platform/deployment/self-managed/installation "Installation" + FileNavigationLeaf /platform/deployment/self-managed/configuration "Configuration" + TableOfContentsNavigation /platform/cloud "Cloud Guide" + index: FileNavigationLeaf /platform/cloud "Cloud Guide" + FolderNavigation /platform/cloud/aws/setup "AWS Setup" + index: FileNavigationLeaf /platform/cloud/aws/setup "AWS Setup" + FolderNavigation /platform/cloud/azure/setup "Azure Setup" + index: FileNavigationLeaf /platform/cloud/azure/setup "Azure Setup" + """ + ); } [Test] diff --git a/tests/Navigation.Tests/Assembler/SiteNavigationTests.cs b/tests/Navigation.Tests/Assembler/SiteNavigationTests.cs index b0e9cbafbb..dd3768e428 100644 --- a/tests/Navigation.Tests/Assembler/SiteNavigationTests.cs +++ b/tests/Navigation.Tests/Assembler/SiteNavigationTests.cs @@ -11,6 +11,7 @@ using Elastic.Documentation.Navigation.Isolated; using Elastic.Documentation.Navigation.Isolated.Node; using Elastic.Documentation.Navigation.Tests.Isolation; +using Elastic.Documentation.Testing; namespace Elastic.Documentation.Navigation.Tests.Assembler; @@ -72,10 +73,34 @@ public void ConstructorCreatesSiteNavigation() var siteContext = SiteNavigationTestFixture.CreateContext(fileSystem, "/checkouts/current/observability"); var navigation = new SiteNavigation(siteNavFile, siteContext, documentationSets, sitePrefix: null); - navigation.Should().NotBeNull(); - navigation.Url.Should().Be("/"); - navigation.NavigationTitle.Should().Be("Docs"); - navigation.NavigationItems.Should().HaveCount(2); + NavigationDump + .ToText(navigation) + .Should() + .MatchInlineSnapshot( + """ + SiteNavigation / "Docs" + index: FileNavigationLeaf /serverless/observability "Serverless Observability" + DocumentationSetNavigation /serverless/observability "Serverless Observability" [island] + index: FileNavigationLeaf /serverless/observability "Serverless Observability" + FolderNavigation /serverless/observability/getting-started/quick-start "Quick Start" + index: FileNavigationLeaf /serverless/observability/getting-started/quick-start "Quick Start" + FileNavigationLeaf /serverless/observability/getting-started/installation "Installation" + FolderNavigation /serverless/observability/monitoring "Monitoring" + index: FileNavigationLeaf /serverless/observability/monitoring "Monitoring" + FileNavigationLeaf /serverless/observability/monitoring/logs "Logs" + FileNavigationLeaf /serverless/observability/monitoring/metrics "Metrics" + FileNavigationLeaf /serverless/observability/monitoring/traces "Traces" + DocumentationSetNavigation /serverless/search "Serverless Search" [island] + index: FileNavigationLeaf /serverless/search "Serverless Search" + FolderNavigation /serverless/search/indexing "Indexing" + index: FileNavigationLeaf /serverless/search/indexing "Indexing" + FileNavigationLeaf /serverless/search/indexing/documents "Documents" + FileNavigationLeaf /serverless/search/indexing/bulk-api "Bulk API" + FolderNavigation /serverless/search/searching "Searching" + index: FileNavigationLeaf /serverless/search/searching "Searching" + FileNavigationLeaf /serverless/search/searching/query-dsl "Query DSL" + """ + ); } [Test] @@ -115,10 +140,28 @@ public void SiteNavigationWithNestedChildren() var siteContext = SiteNavigationTestFixture.CreateContext(fileSystem, "/checkouts/current/platform"); var navigation = new SiteNavigation(siteNavFile, siteContext, documentationSets, sitePrefix: null); - navigation.NavigationItems.Should().HaveCount(1); - - var platform = navigation.NavigationItems.First(); - platform.Should().NotBeNull(); + NavigationDump + .ToText(navigation) + .Should() + .MatchInlineSnapshot( + """ + SiteNavigation / "Docs" + index: FileNavigationLeaf /platform "Platform" + DocumentationSetNavigation /platform "Platform" [island] + index: FileNavigationLeaf /platform "Platform" + TableOfContentsNavigation /platform "Deployment Guide" + index: FileNavigationLeaf /platform "Deployment Guide" + FolderNavigation /platform/self-managed/installation "Installation" + index: FileNavigationLeaf /platform/self-managed/installation "Installation" + FileNavigationLeaf /platform/self-managed/configuration "Configuration" + TableOfContentsNavigation /platform "Cloud Guide" + index: FileNavigationLeaf /platform "Cloud Guide" + FolderNavigation /platform/aws/setup "AWS Setup" + index: FileNavigationLeaf /platform/aws/setup "AWS Setup" + FolderNavigation /platform/azure/setup "Azure Setup" + index: FileNavigationLeaf /platform/azure/setup "Azure Setup" + """ + ); } [Test] diff --git a/tests/Navigation.Tests/DesignSystem/DocsBarRenderingTests.cs b/tests/Navigation.Tests/DesignSystem/DocsBarRenderingTests.cs index 43296e29c3..072fd0d322 100644 --- a/tests/Navigation.Tests/DesignSystem/DocsBarRenderingTests.cs +++ b/tests/Navigation.Tests/DesignSystem/DocsBarRenderingTests.cs @@ -30,47 +30,11 @@ public class DocsBarRenderingTests : DocumentationSetNavigationTestBase """[{"name":"9.0","href":"/docs/9.0","disabled":false},{"name":"8.19","href":"/docs/8.19","disabled":false}]"""; [Test] - public async Task DocsBar_MarksTheActiveSection_AndRendersDropdownAndExternalLinks() + public async Task DocsBar_RendersTheActiveSectionDropdownExternalSlotSearchAndThemeSwitch() { var html = await RenderHeader(airGapped: false); - html.Should().Contain("id=\"docs-bar\""); - html.Should().Contain("href=\"/docs/guides\" aria-current=\"page\"", "the guides section is current"); - html.Should().NotContain("href=\"/docs/reference\" aria-current=\"page\""); - html.Should().Contain("
"); - html.Should().Contain("data-popover-panel", "the menu is shown in the top layer, so the scrolling tab list cannot clip it"); - html.Should().Contain("Elasticsearch"); - html.Should().Contain("rel=\"noopener noreferrer\"").And.Contain("(opens in a new tab)"); - } - - [Test] - public async Task DocsBar_KeepsTheExternalNavigationSlot() - { - var html = await RenderHeader(airGapped: false); - - html.Should().Contain("id=\"elastic-nav-wrapper\"").And.Contain("hx-preserve").And.Contain("id=\"elastic-nav\""); - html - .IndexOf("id=\"elastic-nav\"", StringComparison.Ordinal) - .Should() - .BeLessThan(html.IndexOf("id=\"docs-bar\"", StringComparison.Ordinal)); - } - - [Test] - public async Task DocsBar_RendersTheSearchDialogThatTheShortcutAndTheNotFoundPageOpen() - { - var html = await RenderHeader(airGapped: false); - - html.Should().Contain("data-search-dialog"); - } - - [Test] - public async Task DocsBar_CarriesTheThemeSwitchAndLeavesTheVersionPickerToTheSidebar() - { - var html = await RenderHeader(airGapped: false); - - html.Should().Contain("theme-switch").And.Contain("data-pref=\"contrast\""); - html.Should().NotContain("All versions", "the version picker sits at the top of the sidebar, not in the bar"); - html.Should().NotContain(" RenderHeader(bool airGapped) diff --git a/tests/Navigation.Tests/DesignSystem/NavSideRenderingTests.cs b/tests/Navigation.Tests/DesignSystem/NavSideRenderingTests.cs index 276b790158..728dea54b7 100644 --- a/tests/Navigation.Tests/DesignSystem/NavSideRenderingTests.cs +++ b/tests/Navigation.Tests/DesignSystem/NavSideRenderingTests.cs @@ -21,9 +21,7 @@ public async Task IslandSidebar_ShowsBackLinksAsPillsAndTheSectionTitleAsAHeadin { var html = await RenderAsync(NavSideExamples.Navigation); - html.Should().Contain("class=\"nav-side__back\""); - html.Should().Contain("nav-side__heading nav-side__heading--island"); - html.Should().Contain("nav-tree__link--island"); + html.Should().MatchHtmlSnapshot(DocsHtml.Body()); } [Test] @@ -31,8 +29,7 @@ public async Task SectionSidebar_WithoutBackLinks_KeepsTheQuietLabel() { var html = await RenderAsync(NavSideExamples.Navigation with { BackLinks = [] }); - html.Should().Contain("class=\"nav-side__heading\""); - html.Should().NotContain("nav-side__heading--island"); + html.Should().MatchHtmlSnapshot(DocsHtml.Body()); } [Test] @@ -47,6 +44,6 @@ public async Task Tree_MarksAbsoluteLinksAsExternal() }; var html = await RenderAsync(NavSideExamples.Navigation with { Tree = [external] }); - html.Should().Contain("nav-tree__link--external").And.Contain("target=\"_blank\"").And.Contain("rel=\"noopener\""); + html.Should().MatchHtmlSnapshot(DocsHtml.Body()); } } diff --git a/tests/Navigation.Tests/Isolation/DeepLinkedIndexFileTests.cs b/tests/Navigation.Tests/Isolation/DeepLinkedIndexFileTests.cs index e5b0c94efc..7620e86486 100644 --- a/tests/Navigation.Tests/Isolation/DeepLinkedIndexFileTests.cs +++ b/tests/Navigation.Tests/Isolation/DeepLinkedIndexFileTests.cs @@ -9,6 +9,7 @@ using Elastic.Documentation.Navigation.Isolated; using Elastic.Documentation.Navigation.Isolated.Leaf; using Elastic.Documentation.Navigation.Isolated.Node; +using Elastic.Documentation.Testing; namespace Elastic.Documentation.Navigation.Tests.Isolation; @@ -107,10 +108,18 @@ public async Task ChildlessDeepLinkedIndexFileRendersAsSingleLinkFolder() await context.Collector.StopAsync(TestContext.Current!.Execution.CancellationToken); - var folder = navigation.NavigationItems.OfType>().Single(); - folder.Url.Should().Be("/reference/1password"); - // No other navigation items, so the frontend renders this as a plain link rather than an expandable folder. - folder.NavigationItems.Should().BeEmpty(); + NavigationDump + .ToText(navigation) + .Should() + .MatchInlineSnapshot( + """ + DocumentationSetNavigation / "index" + index: FileNavigationLeaf / "index" + FolderNavigation /reference/1password "index" + index: FileNavigationLeaf /reference/1password "index" + """ + ); + context.Collector.Errors.Should().Be(0); } @@ -140,16 +149,24 @@ public async Task FolderWithChildlessIndexFileChildrenKeepsEveryEntry() await context.Collector.StopAsync(TestContext.Current!.Execution.CancellationToken); - var reference = navigation.NavigationItems.OfType>().Single(); - reference.Url.Should().Be("/reference"); - - // All three entries survive as their own subfolders; none is consumed as the parent's index. - var childFolders = reference.NavigationItems.OfType>().ToList(); - childFolders - .Select(f => f.Url) + NavigationDump + .ToText(navigation) .Should() - .BeEquivalentTo(["/reference/1password", "/reference/abnormal_security", "/reference/activemq"]); - childFolders.Should().AllSatisfy(f => f.NavigationItems.Should().BeEmpty()); + .MatchInlineSnapshot( + """ + DocumentationSetNavigation /reference "index" + index: FileNavigationLeaf /reference "index" + FolderNavigation /reference "index" + index: FileNavigationLeaf /reference "index" + FolderNavigation /reference/1password "index" + index: FileNavigationLeaf /reference/1password "index" + FolderNavigation /reference/abnormal_security "index" + index: FileNavigationLeaf /reference/abnormal_security "index" + FolderNavigation /reference/activemq "index" + index: FileNavigationLeaf /reference/activemq "index" + """ + ); + context.Collector.Errors.Should().Be(0); } @@ -177,13 +194,22 @@ public async Task ChildlessIndexFileChildrenUnderVirtualFileResolveWithoutPathDo await context.Collector.StopAsync(TestContext.Current!.Execution.CancellationToken); - var intro = navigation.NavigationItems.OfType>().Single(); - intro.Url.Should().Be("/reference/apache-intro"); + NavigationDump + .ToText(navigation) + .Should() + .MatchInlineSnapshot( + """ + DocumentationSetNavigation /reference/apache-intro "apache-intro" + index: FileNavigationLeaf /reference/apache-intro "apache-intro" + VirtualFileNavigation /reference/apache-intro "apache-intro" + index: FileNavigationLeaf /reference/apache-intro "apache-intro" + FolderNavigation /reference/apache "index" + index: FileNavigationLeaf /reference/apache "index" + FolderNavigation /reference/apache_spark "index" + index: FileNavigationLeaf /reference/apache_spark "index" + """ + ); - var childFolders = intro.NavigationItems.OfType>().ToList(); - // Deep-linked index paths resolve relative to the virtual file's parent directory, exactly like - // a plain sibling file would (see ProbePlainFileChildUnderVirtualFileWithFullPath for the baseline). - childFolders.Select(f => f.Url).Should().BeEquivalentTo(["/reference/apache", "/reference/apache_spark"]); context.Collector.Errors.Should().Be(0); } @@ -214,9 +240,20 @@ public async Task ChildlessDeepLinkedIndexFileNestedUnderExplicitFolderResolvesP await context.Collector.StopAsync(TestContext.Current!.Execution.CancellationToken); - var reference = navigation.NavigationItems.OfType>().Single(); - var nested = reference.NavigationItems.OfType>().Single(); - nested.Url.Should().Be("/reference/integrations/aws"); + NavigationDump + .ToText(navigation) + .Should() + .MatchInlineSnapshot( + """ + DocumentationSetNavigation /reference "index" + index: FileNavigationLeaf /reference "index" + FolderNavigation /reference "index" + index: FileNavigationLeaf /reference "index" + FolderNavigation /reference/integrations/aws "index" + index: FileNavigationLeaf /reference/integrations/aws "index" + """ + ); + context.Collector.Errors.Should().Be(0); } @@ -245,8 +282,17 @@ public async Task ProbePlainFileChildUnderVirtualFileWithFullPath() await context.Collector.StopAsync(TestContext.Current!.Execution.CancellationToken); - var intro = navigation.NavigationItems.OfType>().Single(); - var child = intro.NavigationItems.Single(); - child.Url.Should().Be("/reference/reference/apache-other"); + NavigationDump + .ToText(navigation) + .Should() + .MatchInlineSnapshot( + """ + DocumentationSetNavigation /reference/apache-intro "apache-intro" + index: FileNavigationLeaf /reference/apache-intro "apache-intro" + VirtualFileNavigation /reference/apache-intro "apache-intro" + index: FileNavigationLeaf /reference/apache-intro "apache-intro" + FileNavigationLeaf /reference/reference/apache-other "apache-other" + """ + ); } } diff --git a/tests/Navigation.Tests/Isolation/DynamicUrlTests.cs b/tests/Navigation.Tests/Isolation/DynamicUrlTests.cs index c24376274c..ab78767c12 100644 --- a/tests/Navigation.Tests/Isolation/DynamicUrlTests.cs +++ b/tests/Navigation.Tests/Isolation/DynamicUrlTests.cs @@ -9,6 +9,7 @@ using Elastic.Documentation.Navigation.Isolated; using Elastic.Documentation.Navigation.Isolated.Leaf; using Elastic.Documentation.Navigation.Isolated.Node; +using Elastic.Documentation.Testing; namespace Elastic.Documentation.Navigation.Tests.Isolation; @@ -110,10 +111,18 @@ public void FolderWithoutIndexUsesFirstChildUrl() var docSet = DocumentationSetFile.LoadAndResolve(context.Collector, yaml, fileSystem.NewDirInfo("docs")); var navigation = new DocumentationSetNavigation(docSet, context, GenericDocumentationFileFactory.Instance); - var folder = navigation.NavigationItems.First() as FolderNavigation; - - // Folder has no index.md, so URL should be the first child's URL - folder!.Url.Should().Be("/guides/getting-started"); + NavigationDump + .ToText(navigation) + .Should() + .MatchInlineSnapshot( + """ + DocumentationSetNavigation /guides/getting-started "getting-started" + index: FileNavigationLeaf /guides/getting-started "getting-started" + FolderNavigation /guides/getting-started "getting-started" + index: FileNavigationLeaf /guides/getting-started "getting-started" + FileNavigationLeaf /guides/advanced "advanced" + """ + ); } [Test] @@ -136,12 +145,18 @@ public void FolderWithoutIndexUsesFirstVisibleChildUrlWhenHiddenChildComesFirst( var docSet = DocumentationSetFile.LoadAndResolve(context.Collector, yaml, fileSystem.NewDirInfo("docs")); var navigation = new DocumentationSetNavigation(docSet, context, GenericDocumentationFileFactory.Instance); - var folder = navigation.NavigationItems.First().Should().BeOfType>().Subject; - - folder.Hidden.Should().BeFalse(); - folder.NavigationTitle.Should().Be("getting-started"); - folder.Url.Should().Be("/guides/getting-started"); - folder.NavigationItems.Should().ContainSingle().Which.Hidden.Should().BeTrue(); + NavigationDump + .ToText(navigation) + .Should() + .MatchInlineSnapshot( + """ + DocumentationSetNavigation /guides/getting-started "getting-started" + index: FileNavigationLeaf /guides/getting-started "getting-started" + FolderNavigation /guides/getting-started "getting-started" + index: FileNavigationLeaf /guides/getting-started "getting-started" + FileNavigationLeaf /guides/autopilot "autopilot" [hidden] + """ + ); } [Test] @@ -163,11 +178,17 @@ public void FolderWithoutIndexAndOnlyHiddenChildrenIsHidden() var docSet = DocumentationSetFile.LoadAndResolve(context.Collector, yaml, fileSystem.NewDirInfo("docs")); var navigation = new DocumentationSetNavigation(docSet, context, GenericDocumentationFileFactory.Instance); - var folder = navigation.NavigationItems.First().Should().BeOfType>().Subject; - - folder.Hidden.Should().BeTrue(); - folder.Index.Hidden.Should().BeTrue(); - folder.NavigationItems.Should().BeEmpty(); + NavigationDump + .ToText(navigation) + .Should() + .MatchInlineSnapshot( + """ + DocumentationSetNavigation /guides/autopilot "autopilot" + index: FileNavigationLeaf /guides/autopilot "autopilot" [hidden] + FolderNavigation /guides/autopilot "autopilot" [hidden] + index: FileNavigationLeaf /guides/autopilot "autopilot" [hidden] + """ + ); } [Test] @@ -191,20 +212,20 @@ public void FolderWithNestedChildren() var docSet = DocumentationSetFile.LoadAndResolve(context.Collector, yaml, fileSystem.NewDirInfo("docs")); var navigation = new DocumentationSetNavigation(docSet, context, GenericDocumentationFileFactory.Instance); - var folder = navigation.NavigationItems.First() as FolderNavigation; - - // Folder has no index.md, so URL should be the first child's URL - folder!.Url.Should().Be("/guides/getting-started"); - - var gettingStarted = folder.NavigationItems.First() as VirtualFileNavigation; - gettingStarted.Should().NotBeNull(); - gettingStarted.Url.Should().Be("/guides/getting-started"); - var advanced = gettingStarted.NavigationItems.First() as FileNavigationLeaf; - advanced.Should().NotBeNull(); - advanced.Url.Should().Be("/guides/advanced"); - - advanced.Parent.Should().BeSameAs(gettingStarted); - gettingStarted.Parent.Should().BeSameAs(folder); + NavigationDump + .ToText(navigation) + .Should() + .MatchInlineSnapshot( + """ + DocumentationSetNavigation /guides/getting-started "getting-started" + index: FileNavigationLeaf /guides/getting-started "getting-started" + FolderNavigation /guides/getting-started "getting-started" + index: FileNavigationLeaf /guides/getting-started "getting-started" + VirtualFileNavigation /guides/getting-started "getting-started" + index: FileNavigationLeaf /guides/getting-started "getting-started" + FileNavigationLeaf /guides/advanced "advanced" + """ + ); } [Test] @@ -228,20 +249,20 @@ public void FolderWithNestedDeeplinkedChildren() var docSet = DocumentationSetFile.LoadAndResolve(context.Collector, yaml, fileSystem.NewDirInfo("docs")); var navigation = new DocumentationSetNavigation(docSet, context, GenericDocumentationFileFactory.Instance); - var folder = navigation.NavigationItems.First() as FolderNavigation; - - // Folder has no index.md, so URL should be the first child's URL - folder!.Url.Should().Be("/guides/clients/getting-started"); - - var gettingStarted = folder.NavigationItems.First() as VirtualFileNavigation; - gettingStarted.Should().NotBeNull(); - gettingStarted.Url.Should().Be("/guides/clients/getting-started"); - var advanced = gettingStarted.NavigationItems.First() as FileNavigationLeaf; - advanced.Should().NotBeNull(); - advanced.Url.Should().Be("/guides/advanced"); - - advanced.Parent.Should().BeSameAs(gettingStarted); - gettingStarted.Parent.Should().BeSameAs(folder); + NavigationDump + .ToText(navigation) + .Should() + .MatchInlineSnapshot( + """ + DocumentationSetNavigation /guides/clients/getting-started "getting-started" + index: FileNavigationLeaf /guides/clients/getting-started "getting-started" + FolderNavigation /guides/clients/getting-started "getting-started" + index: FileNavigationLeaf /guides/clients/getting-started "getting-started" + VirtualFileNavigation /guides/clients/getting-started "getting-started" + index: FileNavigationLeaf /guides/clients/getting-started "getting-started" + FileNavigationLeaf /guides/advanced "advanced" + """ + ); } [Test] @@ -265,20 +286,20 @@ public void FolderWithNestedDeeplinkedOfIndexChildren() var docSet = DocumentationSetFile.LoadAndResolve(context.Collector, yaml, fileSystem.NewDirInfo("docs")); var navigation = new DocumentationSetNavigation(docSet, context, GenericDocumentationFileFactory.Instance); - var folder = navigation.NavigationItems.First() as FolderNavigation; - - // Folder has no index.md, so URL should be the first child's URL - folder!.Url.Should().Be("/guides/clients"); - - var gettingStarted = folder.NavigationItems.First() as VirtualFileNavigation; - gettingStarted.Should().NotBeNull(); - gettingStarted.Url.Should().Be("/guides/clients"); - var advanced = gettingStarted.NavigationItems.First() as FileNavigationLeaf; - advanced.Should().NotBeNull(); - advanced.Url.Should().Be("/guides/advanced"); - - advanced.Parent.Should().BeSameAs(gettingStarted); - gettingStarted.Parent.Should().BeSameAs(folder); + NavigationDump + .ToText(navigation) + .Should() + .MatchInlineSnapshot( + """ + DocumentationSetNavigation /guides/clients "index" + index: FileNavigationLeaf /guides/clients "index" + FolderNavigation /guides/clients "index" + index: FileNavigationLeaf /guides/clients "index" + VirtualFileNavigation /guides/clients "index" + index: FileNavigationLeaf /guides/clients "index" + FileNavigationLeaf /guides/advanced "advanced" + """ + ); } [Test] @@ -301,10 +322,18 @@ public void FolderWithIndexUsesOwnUrl() var docSet = DocumentationSetFile.LoadAndResolve(context.Collector, yaml, fileSystem.NewDirInfo("docs")); var navigation = new DocumentationSetNavigation(docSet, context, GenericDocumentationFileFactory.Instance); - var folder = navigation.NavigationItems.First() as FolderNavigation; - - // Folder has index.md, so URL should be the folder path - folder!.Url.Should().Be("/guides"); + NavigationDump + .ToText(navigation) + .Should() + .MatchInlineSnapshot( + """ + DocumentationSetNavigation /guides "index" + index: FileNavigationLeaf /guides "index" + FolderNavigation /guides "index" + index: FileNavigationLeaf /guides "index" + FileNavigationLeaf /guides/advanced "advanced" + """ + ); } [Test] diff --git a/tests/Navigation.Tests/Isolation/FileNavigationTests.cs b/tests/Navigation.Tests/Isolation/FileNavigationTests.cs index 1164307efb..cb31ff1330 100644 --- a/tests/Navigation.Tests/Isolation/FileNavigationTests.cs +++ b/tests/Navigation.Tests/Isolation/FileNavigationTests.cs @@ -9,6 +9,7 @@ using Elastic.Documentation.Navigation.Isolated; using Elastic.Documentation.Navigation.Isolated.Leaf; using Elastic.Documentation.Navigation.Isolated.Node; +using Elastic.Documentation.Testing; namespace Elastic.Documentation.Navigation.Tests.Isolation; @@ -32,9 +33,15 @@ public void FileWithNoChildrenCreatesFileNavigationLeaf() var navigation = new DocumentationSetNavigation(docSet, context, GenericDocumentationFileFactory.Instance); - navigation.NavigationItems.Should().HaveCount(0); - var fileNav = navigation.Index.Should().BeOfType>().Subject; - fileNav.Url.Should().Be("/getting-started"); + NavigationDump + .ToText(navigation) + .Should() + .MatchInlineSnapshot( + """ + DocumentationSetNavigation /getting-started "getting-started" + index: FileNavigationLeaf /getting-started "getting-started" + """ + ); } [Test] @@ -58,18 +65,19 @@ public void FileWithChildrenCreatesFileNavigation() var navigation = new DocumentationSetNavigation(docSet, context, GenericDocumentationFileFactory.Instance); - navigation.NavigationItems.Should().HaveCount(1); - var fileNav = navigation.NavigationItems.First().Should().BeOfType>().Subject; - fileNav.Url.Should().Be("/guide"); - fileNav.NavigationItems.Should().HaveCount(2); - - var section1 = fileNav.NavigationItems.ElementAt(0).Should().BeOfType>().Subject; - section1.Url.Should().Be("/section1"); - section1.Parent.Should().BeSameAs(fileNav); - - var section2 = fileNav.NavigationItems.ElementAt(1).Should().BeOfType>().Subject; - section2.Url.Should().Be("/section2"); - section2.Parent.Should().BeSameAs(fileNav); + NavigationDump + .ToText(navigation) + .Should() + .MatchInlineSnapshot( + """ + DocumentationSetNavigation /guide "guide" + index: FileNavigationLeaf /guide "guide" + VirtualFileNavigation /guide "guide" + index: FileNavigationLeaf /guide "guide" + FileNavigationLeaf /section1 "section1" + FileNavigationLeaf /section2 "section2" + """ + ); } [Test] @@ -93,18 +101,19 @@ public void FileWithChildrenDeeplinksPreservesPaths() var navigation = new DocumentationSetNavigation(docSet, context, GenericDocumentationFileFactory.Instance); - navigation.NavigationItems.Should().HaveCount(1); - var fileNav = navigation.NavigationItems.First().Should().BeOfType>().Subject; - fileNav.Url.Should().Be("/nest/guide"); - fileNav.NavigationItems.Should().HaveCount(2); - - var section1 = fileNav.NavigationItems.ElementAt(0).Should().BeOfType>().Subject; - section1.Url.Should().Be("/nest/section1"); - section1.Parent.Should().BeSameAs(fileNav); - - var section2 = fileNav.NavigationItems.ElementAt(1).Should().BeOfType>().Subject; - section2.Url.Should().Be("/nest/section2"); - section2.Parent.Should().BeSameAs(fileNav); + NavigationDump + .ToText(navigation) + .Should() + .MatchInlineSnapshot( + """ + DocumentationSetNavigation /nest/guide "guide" + index: FileNavigationLeaf /nest/guide "guide" + VirtualFileNavigation /nest/guide "guide" + index: FileNavigationLeaf /nest/guide "guide" + FileNavigationLeaf /nest/section1 "section1" + FileNavigationLeaf /nest/section2 "section2" + """ + ); } [Test] @@ -129,19 +138,20 @@ public void FileWithNestedChildrenBuildsCorrectly() var navigation = new DocumentationSetNavigation(docSet, context, GenericDocumentationFileFactory.Instance); - navigation.NavigationItems.Should().HaveCount(1); - var guideFile = navigation.NavigationItems.First().Should().BeOfType>().Subject; - guideFile.Url.Should().Be("/guide"); - guideFile.NavigationItems.Should().HaveCount(1); - - var chapter1 = guideFile.NavigationItems.First().Should().BeOfType>().Subject; - chapter1.Url.Should().Be("/chapter1"); - chapter1.Parent.Should().BeSameAs(guideFile); - chapter1.NavigationItems.Should().HaveCount(1); - - var subsection = chapter1.NavigationItems.First().Should().BeOfType>().Subject; - subsection.Url.Should().Be("/subsection"); - subsection.Parent.Should().BeSameAs(chapter1); + NavigationDump + .ToText(navigation) + .Should() + .MatchInlineSnapshot( + """ + DocumentationSetNavigation /guide "guide" + index: FileNavigationLeaf /guide "guide" + VirtualFileNavigation /guide "guide" + index: FileNavigationLeaf /guide "guide" + VirtualFileNavigation /chapter1 "chapter1" + index: FileNavigationLeaf /chapter1 "chapter1" + FileNavigationLeaf /subsection "subsection" + """ + ); } [Test] @@ -201,17 +211,19 @@ public void FileNavigationMixedWithFolderChildren() var navigation = new DocumentationSetNavigation(docSet, context, GenericDocumentationFileFactory.Instance); - var guideFile = navigation.NavigationItems.First().Should().BeOfType>().Subject; - guideFile.NavigationItems.Should().HaveCount(2); - - var intro = guideFile.NavigationItems.ElementAt(0).Should().BeOfType>().Subject; - intro.Url.Should().Be("/intro"); - - var advancedFolder = guideFile.NavigationItems.ElementAt(1).Should().BeOfType>().Subject; - advancedFolder.Url.Should().Be("/advanced/topics"); // No index, uses first child - advancedFolder.NavigationItems.Should().HaveCount(0); - - var topics = advancedFolder.Index.Should().BeOfType>().Subject; - topics.Url.Should().Be("/advanced/topics"); + NavigationDump + .ToText(navigation) + .Should() + .MatchInlineSnapshot( + """ + DocumentationSetNavigation /guide "guide" + index: FileNavigationLeaf /guide "guide" + VirtualFileNavigation /guide "guide" + index: FileNavigationLeaf /guide "guide" + FileNavigationLeaf /intro "intro" + FolderNavigation /advanced/topics "topics" + index: FileNavigationLeaf /advanced/topics "topics" + """ + ); } } diff --git a/tests/Navigation.Tests/Isolation/FolderIndexFileRefTests.cs b/tests/Navigation.Tests/Isolation/FolderIndexFileRefTests.cs index a5056178ec..265d8f4ee6 100644 --- a/tests/Navigation.Tests/Isolation/FolderIndexFileRefTests.cs +++ b/tests/Navigation.Tests/Isolation/FolderIndexFileRefTests.cs @@ -9,6 +9,7 @@ using Elastic.Documentation.Extensions; using Elastic.Documentation.Navigation.Isolated; using Elastic.Documentation.Navigation.Isolated.Node; +using Elastic.Documentation.Testing; namespace Elastic.Documentation.Navigation.Tests.Isolation; @@ -40,14 +41,20 @@ public async Task FolderWithFileCreatesCorrectStructure() await context.Collector.StopAsync(TestContext.Current!.Execution.CancellationToken); // Should create a FolderNavigation with the file as index - navigation.NavigationItems.Should().HaveCount(1); - var folder = navigation.NavigationItems.First().Should().BeOfType>().Subject; - - // Children should be scoped to the folder - folder.Url.Should().Be("/getting-started/getting-started"); - folder.NavigationItems.Should().HaveCount(2); // install.md, configure.md + NavigationDump + .ToText(navigation) + .Should() + .MatchInlineSnapshot( + """ + DocumentationSetNavigation /getting-started/getting-started "getting-started" + index: FileNavigationLeaf /getting-started/getting-started "getting-started" + FolderNavigation /getting-started/getting-started "getting-started" + index: FileNavigationLeaf /getting-started/getting-started "getting-started" + FileNavigationLeaf /getting-started/install "install" + FileNavigationLeaf /getting-started/configure "configure" + """ + ); - // Verify no errors context.Collector.Errors.Should().Be(0); } diff --git a/tests/Navigation.Tests/Isolation/NavigationStructureTests.cs b/tests/Navigation.Tests/Isolation/NavigationStructureTests.cs index 901c05737a..f5d85fae7c 100644 --- a/tests/Navigation.Tests/Isolation/NavigationStructureTests.cs +++ b/tests/Navigation.Tests/Isolation/NavigationStructureTests.cs @@ -9,6 +9,7 @@ using Elastic.Documentation.Navigation.Isolated; using Elastic.Documentation.Navigation.Isolated.Leaf; using Elastic.Documentation.Navigation.Isolated.Node; +using Elastic.Documentation.Testing; namespace Elastic.Documentation.Navigation.Tests.Isolation; @@ -170,54 +171,25 @@ public async Task ComplexNestedStructureBuildsCorrectly() await context.Collector.StopAsync(TestContext.Current!.Execution.CancellationToken); - navigation.NavigationItems.Should().HaveCount(2); navigation.IsUsingNavigationDropdown.Should().BeTrue(); - - // First item: simple file - var indexFile = navigation.Index.Should().BeOfType>().Subject; - indexFile.Url.Should().Be("/"); // index.md becomes / - - // Second item: complex nested structure - var setupFolder = navigation.NavigationItems.ElementAt(0).Should().BeOfType>().Subject; - setupFolder.NavigationItems.Should().HaveCount(1); - setupFolder.Url.Should().Be("/setup"); - - var setupIndex = setupFolder.Index.Should().BeOfType>().Subject; - setupIndex.Url.Should().Be("/setup"); // index.md becomes /setup - - var advancedToc = setupFolder - .NavigationItems - .ElementAt(0) - .Should() - .BeOfType>() - .Subject; - advancedToc.Url.Should().Be("/setup/advanced"); - // Advanced TOC has index.md and the nested performance TOC as children - advancedToc.NavigationItems.Should().HaveCount(1); - - var advancedIndex = advancedToc.Index.Should().BeOfType>().Subject; - advancedIndex.Url.Should().Be("/setup/advanced"); - - var performanceToc = advancedToc - .NavigationItems - .ElementAt(0) + NavigationDump + .ToText(navigation) .Should() - .BeOfType>() - .Subject; - performanceToc.Url.Should().Be("/setup/advanced/performance"); - performanceToc.NavigationItems.Should().HaveCount(2); - - var performanceIndex = performanceToc.Index.Should().BeOfType>().Subject; - performanceIndex.Url.Should().Be("/setup/advanced/performance"); - - var tuning = performanceToc.NavigationItems.ElementAt(0).Should().BeOfType>().Subject; - tuning.Url.Should().Be("/setup/advanced/performance/tuning"); - - var benchmarks = performanceToc.NavigationItems.ElementAt(1).Should().BeOfType>().Subject; - benchmarks.Url.Should().Be("/setup/advanced/performance/benchmarks"); - - // Third item: crosslink - _ = navigation.NavigationItems.ElementAt(1).Should().BeOfType().Subject; + .MatchInlineSnapshot( + """ + DocumentationSetNavigation / "index" + index: FileNavigationLeaf / "index" + FolderNavigation /setup "index" + index: FileNavigationLeaf /setup "index" + TableOfContentsNavigation /setup/advanced "index" + index: FileNavigationLeaf /setup/advanced "index" + TableOfContentsNavigation /setup/advanced/performance "index" + index: FileNavigationLeaf /setup/advanced/performance "index" + FileNavigationLeaf /setup/advanced/performance/tuning "tuning" + FileNavigationLeaf /setup/advanced/performance/benchmarks "benchmarks" + CrossLinkNavigationLeaf https://docs-v3-preview.elastic.dev/elastic/docs-builder/tree/main "External" + """ + ); // Verify no errors were emitted context.Diagnostics.Should().BeEmpty(); @@ -267,32 +239,22 @@ public void NestedTocUrlsDoNotDuplicatePath() var navigation = new DocumentationSetNavigation(docSet, context, TestDocumentationFileFactory.Instance); - var setupFolder = navigation.NavigationItems.First().Should().BeOfType>().Subject; - setupFolder.Url.Should().Be("/setup"); - - // Setup folder has index.md and advanced TOC - setupFolder.NavigationItems.Should().HaveCount(1); - - var advancedToc = setupFolder - .NavigationItems - .ElementAt(0) + // the urls are /setup/advanced and /setup/advanced/performance, not /setup/setup/advanced and so on + NavigationDump + .ToText(navigation) .Should() - .BeOfType>() - .Subject; - // Verify the URL is /setup/advanced and not /setup/setup/advanced - advancedToc.Url.Should().Be("/setup/advanced"); - - // Advanced TOC has index.md and performance TOC - advancedToc.NavigationItems.Should().HaveCount(1); - - var performanceToc = advancedToc - .NavigationItems - .ElementAt(0) - .Should() - .BeOfType>() - .Subject; - // Verify the URL is /setup/advanced/performance and not /setup/advanced/setup/advanced/performance - performanceToc.Url.Should().Be("/setup/advanced/performance"); + .MatchInlineSnapshot( + """ + DocumentationSetNavigation /setup "index" + index: FileNavigationLeaf /setup "index" + FolderNavigation /setup "index" + index: FileNavigationLeaf /setup "index" + TableOfContentsNavigation /setup/advanced "index" + index: FileNavigationLeaf /setup/advanced "index" + TableOfContentsNavigation /setup/advanced/performance "index" + index: FileNavigationLeaf /setup/advanced/performance "index" + """ + ); context.Diagnostics.Should().BeEmpty(); } diff --git a/tests/Navigation.Tests/Navigation.Tests.csproj b/tests/Navigation.Tests/Navigation.Tests.csproj index 34b7b1a734..11654a9b69 100644 --- a/tests/Navigation.Tests/Navigation.Tests.csproj +++ b/tests/Navigation.Tests/Navigation.Tests.csproj @@ -19,6 +19,12 @@ + + + + + + diff --git a/tests/Navigation.Tests/Rendering/FooterRenderingTests.cs b/tests/Navigation.Tests/Rendering/FooterRenderingTests.cs index 200dcd7b40..5da06d48ce 100644 --- a/tests/Navigation.Tests/Rendering/FooterRenderingTests.cs +++ b/tests/Navigation.Tests/Rendering/FooterRenderingTests.cs @@ -22,31 +22,7 @@ public async Task AssemblerFooter_RendersIubendaLinksAfterTrademarkCopy() { var html = await RenderAssemblerFooter(); - html.Should().Contain("iubenda-cs-uspr-link"); - html.Should().Contain("Notice at Collection"); - html.Should().Contain("iubenda-cs-preferences-link"); - html.Should().Contain("Your Privacy Choices"); - html.Should().Contain("privacy-statement"); - html.Should().Contain("Elasticsearch is a trademark"); - - var trademark = html.IndexOf("Elasticsearch is a trademark", StringComparison.Ordinal); - var notice = html.IndexOf("Notice at Collection", StringComparison.Ordinal); - trademark.Should().BeGreaterThan(-1); - notice.Should().BeGreaterThan(-1); - trademark.Should().BeLessThan(notice); - html.IndexOf("", StringComparison.Ordinal).Should().BeLessThan(html.IndexOf("Notice at Collection", StringComparison.Ordinal)); - - var preferences = html.IndexOf("iubenda-cs-preferences-link", StringComparison.Ordinal); - preferences.Should().BeGreaterThan(-1); - var anchorEnd = html.IndexOf("", preferences, StringComparison.Ordinal); - anchorEnd.Should().BeGreaterThan(preferences); - var preferencesLink = html[preferences..anchorEnd]; - preferencesLink.Should().Contain("Your Privacy Choices"); - preferencesLink.Should().Contain(" RenderAssemblerFooter() diff --git a/tests/Navigation.Tests/Rendering/SecondaryNavRenderingTests.cs b/tests/Navigation.Tests/Rendering/SecondaryNavRenderingTests.cs index 425b3e0b4c..5cefe37e8d 100644 --- a/tests/Navigation.Tests/Rendering/SecondaryNavRenderingTests.cs +++ b/tests/Navigation.Tests/Rendering/SecondaryNavRenderingTests.cs @@ -45,29 +45,15 @@ public async Task WithoutConfigurationTheBuiltInLinksAreRendered() { var html = await Render(topNav: null, currentUrl: "/docs/"); - // Built-in links present - html.Should().Contain("Release notes").And.Contain("Troubleshoot").And.Contain("Reference"); - html.Should().Contain("href=\"#icon-refresh-time\""); - html.Should().Contain("href=\"#icon-wrench\""); - html.Should().Contain("href=\"#icon-list-bullet\""); - html.Should().NotContain("secondary-nav-dropdown"); - html.Should().NotContain("id=\"htmx-indicator\""); - html.Should().Contain("id=\"secondary-nav-host\""); - html.Should().NotContain("hx-preserve"); - html.Should().Contain("secondary-nav-home").And.Contain(">Docs<"); - html.Should().Contain("href=\"/docs/\""); + html.Should().MatchHtmlSnapshot(DocsHtml.Body()); } [Test] - public async Task ConfiguredLinksReplaceTheBuiltInOnes() + public async Task ConfiguredTopNavRendersTheDesktopBar() { var html = await Render(TopNav, currentUrl: "/docs/"); - html.Should().Contain("href=\"/docs/reference/\""); - // the built-in links are gone once top_nav is configured - html.Should().NotContain("Release notes").And.NotContain("Troubleshoot"); - html.Should().NotContain("id=\"htmx-indicator\""); - html.Should().Contain("data-section-ids=\"ref-section-id\""); + html.Should().MatchHtmlSnapshot(DocsHtml.Body()); } [Test] @@ -79,22 +65,7 @@ public async Task TopNavLinksRenderInMobileDrawer() root: new MockSectionRoot(ReferenceSectionId) ); - html.Should().Contain("secondary-nav-mobile-menu"); - html.Should().Contain(">Section<"); - html.Should().Contain("Reference"); - html.Should().Contain("href=\"/docs/guides/\""); - html.Should().Contain("href=\"/docs/reference/\""); - html.Should().Contain("href=\"https://www.elastic.co/docs/api/\""); - html.Should().Contain("target=\"_blank\""); - html.Should().Contain("(opens in a new tab)"); - } - - [Test] - public async Task TopNavMobileDrawerUsesDocsHomeFallback() - { - var html = await RenderPagesNav(LinkOnlyTopNav, currentUrl: "/docs/"); - - html.Should().Contain("Docs Home"); + html.Should().MatchHtmlSnapshot(DocsHtml.Body()); } [Test] @@ -102,12 +73,7 @@ public async Task MobileDrawerRendersVersionDropdown() { var html = await RenderPagesNav(topNav: null, currentUrl: "/docs/", showVersionDropdown: true); - html.Should().Contain(">Version<"); - html.Should().Contain("Docs<"); + html.Should().MatchHtmlSnapshot(DocsHtml.Body()); } [Test] - public async Task ConfiguredItemsRenderEuiIcons() + public async Task LinkOnlyTopNavRendersItsLinks() { - var html = await Render(TopNav, currentUrl: "/docs/"); - - html.Should().Contain("href=\"#icon-list-bullet\""); - html.Should().Contain("href=\"#icon-code\""); - html.Should().Contain("href=\"#icon-grid\""); - html.Should().Contain("href=\"#icon-chevron-down\""); - html.Should().Contain("href=\"#icon-external\""); + var html = await Render(LinkOnlyTopNav, currentUrl: "/docs/"); - var linkOnly = await Render(LinkOnlyTopNav, currentUrl: "/docs/"); - linkOnly.Should().Contain("href=\"#icon-documentation\""); + html.Should().MatchHtmlSnapshot(DocsHtml.Body()); } [Test] @@ -140,15 +97,7 @@ public async Task VersionDropdownRendersOnTheRightOfTheTopBar() { var html = await Render(TopNav, currentUrl: "/docs/", showVersionDropdown: true); - html.Should().Contain("secondary-nav-actions"); - html.Should().Contain("main"); - html.Should().Contain("id=\"api-version-switcher\"").And.Contain("href=\"/docs/api/doc/elasticsearch/v8/\""); - html.Should().Contain("api-menu-group-label\">Elastic Stack").And.Contain("role=\"group\""); - // A group without a title gets no label, and the separator sits between the groups. - html.Should().NotContain("id=\"api-hub-group-1\""); - string[] markers = ["id=\"api-version-switcher\"", "Kibana APIs", "Alpha API", "All APIs"]; - var order = markers.Select(t => html.IndexOf(t, StringComparison.Ordinal)).ToArray(); - order.Should().OnlyContain(p => p >= 0).And.BeInAscendingOrder(); - CountOccurrences(html, "api-menu-sep").Should().Be(3); - // The spec download lives in the page actions, not here. - html.Should().NotContain("Download OpenAPI spec"); - html.Should().NotContain("id=\"secondary-nav"); + html.Should().MatchHtmlSnapshot(DocsHtml.Body()); } [Test] @@ -233,9 +163,7 @@ public async Task ApiIdentityTriggerDropsTheLatestSuffixButTheMenuKeepsIt() var html = await _ApiIdentity.Create(bar).RenderAsync(cancellationToken: TestContext.Current!.Execution.CancellationToken); - html.Should().Contain("api-identity-version-label\">v9").And.Contain("version v9\""); - html.Should().Contain(">v9 (latest)"); - CountOccurrences(html, "(latest)").Should().Be(1); + html.Should().MatchHtmlSnapshot(DocsHtml.Body()); } [Test] @@ -245,8 +173,7 @@ public async Task ApiIdentityWithoutVersionsOrOtherApisIsPlainText() var html = await _ApiIdentity.Create(bar).RenderAsync(cancellationToken: TestContext.Current!.Execution.CancellationToken); - html.Should().Contain("api-identity-product--static").And.Contain("Alpha API"); - html.Should().NotContain("popovertarget"); + html.Should().MatchHtmlSnapshot(DocsHtml.Body()); } [Test] @@ -263,11 +190,9 @@ public async Task ApiIdentityWithVersionsOnlyListsTheVersions() var html = await _ApiIdentity.Create(bar).RenderAsync(cancellationToken: TestContext.Current!.Execution.CancellationToken); - html.Should().Contain("id=\"api-version-switcher\"").And.NotContain("All APIs").And.NotContain("api-menu-sep"); + html.Should().MatchHtmlSnapshot(DocsHtml.Body()); } - private static int CountOccurrences(string text, string value) => text.Split(value).Length - 1; - [Test] public async Task DocsBarsCarryNoApiChrome() { @@ -280,51 +205,6 @@ public async Task DocsBarsCarryNoApiChrome() } } - [Test] - public async Task WithTopNavTheBarIsLeftAlignedAndIncludesDocsHome() - { - var html = await Render(TopNav, "/docs/"); - - html.Should().Contain("secondary-nav-home").And.Contain(">Docs<"); - html.Should().Contain("href=\"/docs/\""); - html.Should().Contain("secondary-nav-bar--desktop"); - html.Should().Contain("secondary-nav-scroll-container"); - html.Should().Contain("secondary-nav-bar"); - } - - [Test] - public async Task WithoutTopNavTheBarHasDocsBrandLink() - { - var html = await Render(null, "/docs/"); - - html.Should().Contain("secondary-nav-home").And.Contain(">Docs<"); - html.Should().Contain("href=\"/docs/\""); - } - - [Test] - public async Task ExternalLinksOpenInANewTab() - { - var html = await Render(TopNav, currentUrl: "/docs/"); - - html.Should().Contain("href=\"https://www.elastic.co/docs/api/\""); - html.Should().Contain("target=\"_blank\""); - html.Should().Contain("rel=\"noopener noreferrer\""); - html.Should().Contain("(opens in a new tab)"); - } - - [Test] - public async Task DropdownRendersItsGroupsAndLinks() - { - var html = await Render(TopNav, currentUrl: "/docs/"); - - html.Should().Contain("
"); - html.Should().Contain("secondary-nav-dropdown-group-label\">Stack products"); - html.Should().Contain("href=\"/docs/products/elasticsearch/\""); - html.Should().Contain("href=\"/docs/products/\""); - // the label toggles the panel, it is never a link itself - html.Should().NotContain(""); - html.Should().NotContain("hidden md:block"); + html.Should().MatchHtmlSnapshot(DocsHtml.Body()); } [Test] @@ -40,8 +35,7 @@ public async Task Assembler_FlagOn_OmitsVersionDropdown() { var html = await Render(BuildType.Assembler, showVersionDropdown: true, navigationPreviewEnabled: true); - html.Should().NotContain(" Render(BuildType buildType, bool showVersionDropdown, bool navigationPreviewEnabled) diff --git a/tests/Navigation.Tests/Rendering/TocTreeRenderingTests.cs b/tests/Navigation.Tests/Rendering/TocTreeRenderingTests.cs index 5644e89f84..f8a769b6d4 100644 --- a/tests/Navigation.Tests/Rendering/TocTreeRenderingTests.cs +++ b/tests/Navigation.Tests/Rendering/TocTreeRenderingTests.cs @@ -36,15 +36,7 @@ public async Task TocTree_RendersTheDesignSystemSidebar() var html = await _TocTree.Create(model).RenderAsync(cancellationToken: TestContext.Current!.Execution.CancellationToken); - html.Should().Contain("class=\"nav-side\""); - html.Should().Contain("nav-side__scroll"); - html.Should().Contain("link-menu", "a build with several sections gets a section menu"); - html.Should().Contain("nav-side__back"); - html.Should().Contain("nav-tree__link"); - html.Should().Contain("Guides"); - html.Should().NotContain("pages-nav-v2"); - html.Should().NotContain("navigation-search"); - html.Should().NotContain("hx-preserve"); + html.Should().MatchHtmlSnapshot(DocsHtml.Body()); } [Test] @@ -85,12 +77,7 @@ public async Task FolderRow_KeepsThePageLinkAndTheToggleSeparate() var html = await _TocTree.Create(model).RenderAsync(cancellationToken: TestContext.Current!.Execution.CancellationToken); - var linkStart = html.IndexOf("href=\"/docs/contribute\"", StringComparison.Ordinal); - linkStart.Should().BeGreaterThanOrEqualTo(0); - var linkEnd = html.IndexOf("", linkStart, StringComparison.Ordinal); - html[linkStart..linkEnd].Should().NotContain("chevron", "the page link and the expand control are separate targets"); - html.Should().Contain("