From 62867c6e28548157ab8c60318591d3e26838dda8 Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Thu, 5 Nov 2026 12:26:24 +0100 Subject: [PATCH 01/10] Wire Nullean.Snapshots into every test project The ApiExplorer pilot (#4654) used Nullean.Snapshots.Html and .TUnit 0.2.0 in one project. This makes the library available to all unit and integration test projects at 0.4.0 and adds the shared pieces the next migration steps build on: - tests-shared/Elastic.Documentation.Testing: DocsHtml (page section without the fixture H1 and htmx attributes), DiagnosticsSnapshot, NavigationDump, MockFileSystemDump. A scrubber for temp and repo paths is compiled into each test project. - The snap CLI as a local tool; CI uploads received snapshots when a test run fails; the build fails on orphaned snapshots after a unit run. - AGENTS.md describes the workflow. The PR review prompt no longer claims the repo uses xUnit. The five TestDiagnosticsCollector copies stay: they differ in lifecycle (Write vs HandleItem), so merging them is a behaviour change. DiagnosticsSnapshot works on any of them. Co-Authored-By: Claude Sonnet 5.5 --- .github/workflows/ci.yml | 9 +++ .github/workflows/pr-review.yml | 2 +- AGENTS.md | 6 ++ Directory.Packages.props | 8 ++- build/Targets.fs | 6 ++ docs-builder.slnx | 4 ++ dotnet-tools.json | 7 ++ tests-integration/Directory.Build.props | 7 ++ tests-shared/Directory.Build.props | 11 +++ .../DiagnosticsSnapshot.cs | 32 +++++++++ .../Elastic.Documentation.Testing/DocsHtml.cs | 34 +++++++++ .../Elastic.Documentation.Testing.csproj | 28 ++++++++ .../Linked/SnapshotScrubbing.cs | 39 +++++++++++ .../MockFileSystemDump.cs | 35 ++++++++++ .../NavigationDump.cs | 51 ++++++++++++++ tests/Directory.Build.props | 7 ++ .../Elastic.ApiExplorer.Tests.csproj | 2 - .../Testing/SnapshotHelpersTests.cs | 70 +++++++++++++++++++ 18 files changed, 353 insertions(+), 5 deletions(-) create mode 100644 tests-shared/Directory.Build.props create mode 100644 tests-shared/Elastic.Documentation.Testing/DiagnosticsSnapshot.cs create mode 100644 tests-shared/Elastic.Documentation.Testing/DocsHtml.cs create mode 100644 tests-shared/Elastic.Documentation.Testing/Elastic.Documentation.Testing.csproj create mode 100644 tests-shared/Elastic.Documentation.Testing/Linked/SnapshotScrubbing.cs create mode 100644 tests-shared/Elastic.Documentation.Testing/MockFileSystemDump.cs create mode 100644 tests-shared/Elastic.Documentation.Testing/NavigationDump.cs create mode 100644 tests/Elastic.Documentation.Tests/Testing/SnapshotHelpersTests.cs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 50c34ac2c2..7d4066e86c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -203,6 +203,15 @@ jobs: # --single-target skips the compile dependency; the Compile step above already built everything run: dotnet run --project build -c release -- unit-test --single-target + - name: Upload received snapshots + # A failed snapshot test leaves its received file here. Download it into .artifacts/.snapshots/received/ and run `dotnet snap review`. + if: ${{ failure() && needs.changes.outputs.code == 'true' }} + uses: actions/upload-artifact@v7 + with: + name: received-snapshots-${{ matrix.os }} + path: .artifacts/.snapshots/received/ + if-no-files-found: ignore + - name: Publish AOT if: ${{ needs.changes.outputs.code == 'true' && (github.event_name == 'push' && matrix.os != 'ubuntu-latest') }} # publish containers already validates AOT build run: dotnet run --project build -c release -- publishbinaries diff --git a/.github/workflows/pr-review.yml b/.github/workflows/pr-review.yml index 3c884acdc3..314cf54941 100644 --- a/.github/workflows/pr-review.yml +++ b/.github/workflows/pr-review.yml @@ -27,7 +27,7 @@ jobs: validates cross-repo references, and ships as native AOT binaries for CI. ## Language and frameworks - - C# (.NET) with xUnit v3 + AwesomeAssertions for tests. Do NOT suggest TUnit APIs — the migration hasn't happened yet. + - C# (.NET) with TUnit + AwesomeAssertions for tests. Expected output lives in Nullean.Snapshots snapshots (inline literals and `.snapshots/` files); never suggest accepting snapshots in code or CI. - TypeScript/React frontend under src/Elastic.Documentation.Site/. - Test naming convention: Method_Scenario_Expected. diff --git a/AGENTS.md b/AGENTS.md index 0f31e021e8..feb01f4633 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -65,6 +65,12 @@ Tests live in `tests/` (unit) and `tests-integration/` (integration). - **C#**: TUnit · AwesomeAssertions · FakeItEasy across all test projects. All test projects (unit and integration) use TUnit (wired centrally via `tests/Directory.Build.props` and `tests-integration/Directory.Build.props`; no per-project flag needed). Write new tests using TUnit APIs: `[Test]`, `[Arguments(...)]`, `[MethodDataSource(nameof(X))]`. - **TypeScript**: Vitest +- **Snapshots** (`Nullean.Snapshots`, wired for every test project in `tests/Directory.Build.props`; helpers in `tests-shared/Elastic.Documentation.Testing`): expected output lives in snapshots, not in chains of `Contain` assertions. + - Small values go inline: `x.Should().MatchInlineSnapshot("""...""")`, `MatchInlineHtmlSnapshot`, `MatchInlineJsonSnapshot`. Leave the argument out to write a new one. Large values go to a verified file: `MatchSnapshot()`, `MatchHtmlSnapshot()`, `MatchJsonSnapshot()`, `MatchYamlSnapshot()`. + - Diagnostics: `collector.Diagnostics.ToSnapshotText()` (`DiagnosticsSnapshot`). A clean run is the empty snapshot. + - Navigation trees and generated files: `NavigationDump.ToText(...)`, `MockFileSystemDump.ToText(fs, dir)`. Rendered docs HTML: `DocsHtml.Section()` (the page content, no fixture H1, no htmx attributes) or `DocsHtml.Body()`. + - Accept with the `snap` tool (`dotnet snap status`, `dotnet snap review`, `dotnet snap accept `), or `SNAPSHOT_ACCEPT=1`. Read the diff of the changed `.verified.*` files and inline literals before committing. Nothing is accepted in CI. When a CI test run fails, download the `received-snapshots-*` artifact into `.artifacts/.snapshots/received/` and run `dotnet snap review`. + - Changing a snapshot test's output is a behaviour change. Mutate the code once and confirm the test fails before you commit. - **HTML snapshots** (`Nullean.Snapshots.Html`, used in `Elastic.ApiExplorer.Tests`): when a test would check one rendered block with many `Contain` assertions, compare it to a verified snapshot instead and keep one assertion that states the intent. - Write `html.Should().MatchHtmlSnapshot()`. Narrow it with `new HtmlSnapshotOptions().Select("#some-id")` or `.RemoveAttributes(...)` so unrelated markup and volatile values (bundle names, CI git info) stay out. - Snapshot a partial or one section, not a whole page. Whole-page snapshots break on unrelated changes. diff --git a/Directory.Packages.props b/Directory.Packages.props index 782c52aab0..b1dcf0d2c0 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -127,8 +127,12 @@ - - + + + + + + diff --git a/build/Targets.fs b/build/Targets.fs index a73833190f..432a81e056 100644 --- a/build/Targets.fs +++ b/build/Targets.fs @@ -205,6 +205,12 @@ let private runTests (testSuite: TestSuite) (parsed: ParseResults) = |> Array.toList if not (List.isEmpty failures) then failwithf "Test projects failed:\n%s" (String.concat "\n" failures) + + // Every test project has now run, so the run manifests are complete: a verified snapshot nobody used belongs to a deleted or renamed test. + if testSuite <> Integration then + match exec { exit_code_of "dotnet" "snap" "orphans" } with + | 0 -> () + | _ -> failwith "Orphaned snapshots found. Delete them with `dotnet snap orphans --delete` and commit the removal." let private compressibleExtensions = set [".html"; ".css"; ".js"; ".json"; ".svg"; ".xml"; ".txt"] diff --git a/docs-builder.slnx b/docs-builder.slnx index 47468e9ede..31e5d66687 100644 --- a/docs-builder.slnx +++ b/docs-builder.slnx @@ -90,6 +90,10 @@ + + + + diff --git a/dotnet-tools.json b/dotnet-tools.json index 084330e010..d77fdfb621 100644 --- a/dotnet-tools.json +++ b/dotnet-tools.json @@ -43,6 +43,13 @@ "curb" ], "rollForward": false + }, + "nullean.snapshots.cli": { + "version": "0.4.0", + "commands": [ + "snap" + ], + "rollForward": false } } } diff --git a/tests-integration/Directory.Build.props b/tests-integration/Directory.Build.props index 379438ea3e..1717682543 100644 --- a/tests-integration/Directory.Build.props +++ b/tests-integration/Directory.Build.props @@ -18,6 +18,13 @@ + + + + + + + diff --git a/tests-shared/Directory.Build.props b/tests-shared/Directory.Build.props new file mode 100644 index 0000000000..5b92d69384 --- /dev/null +++ b/tests-shared/Directory.Build.props @@ -0,0 +1,11 @@ + + + + + + CA1822;CA1707;IDE0058 + false + false + + diff --git a/tests-shared/Elastic.Documentation.Testing/DiagnosticsSnapshot.cs b/tests-shared/Elastic.Documentation.Testing/DiagnosticsSnapshot.cs new file mode 100644 index 0000000000..0e8f94f1f2 --- /dev/null +++ b/tests-shared/Elastic.Documentation.Testing/DiagnosticsSnapshot.cs @@ -0,0 +1,32 @@ +// 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.Diagnostics; + +namespace Elastic.Documentation.Testing; + +public static class DiagnosticsSnapshot +{ + /// + /// One line per diagnostic, error|warning|hint file:line message, in the order they were written. Empty when + /// there are none, so a clean run reads as MatchInlineSnapshot(""). + /// + public static string ToSnapshotText(this IEnumerable diagnostics) + { + var sb = new StringBuilder(); + foreach (var d in diagnostics) + { + var severity = d.Severity switch + { + Severity.Error => "error", + Severity.Warning => "warning", + _ => "hint" + }; + var position = d.Line is { } line ? $"{d.File}:{line}" : d.File; + sb.Append(severity).Append(' ').Append(position).Append(' ').Append(d.Message).Append('\n'); + } + return sb.ToString(); + } +} diff --git a/tests-shared/Elastic.Documentation.Testing/DocsHtml.cs b/tests-shared/Elastic.Documentation.Testing/DocsHtml.cs new file mode 100644 index 0000000000..961b026997 --- /dev/null +++ b/tests-shared/Elastic.Documentation.Testing/DocsHtml.cs @@ -0,0 +1,34 @@ +// 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 Nullean.Snapshots.Html; + +namespace Elastic.Documentation.Testing; + +/// +/// How docs-builder's tests normalise rendered HTML before comparing or snapshotting it. One place for what +/// Elastic.Markdown.Tests.PrettyHtmlExtensions and Elastic.Authoring.Tests.Framework.HtmlAssertions +/// used to do separately. +/// +public static class DocsHtml +{ + /// htmx navigation attributes on links are plumbing, not content. + private static readonly string[] HtmxAttributes = ["hx-get", "hx-select-oob", "hx-swap", "hx-indicator", "hx-push-url", "preload"]; + + /// The whole body, with htmx attributes removed from links. + public static HtmlSnapshotOptions Body() => new HtmlSnapshotOptions().RemoveAttributesFrom("a", HtmxAttributes); + + /// + /// The rendered page's content section, without the leading H1 the test fixtures wrap every document in + /// (# Test Document), and with htmx attributes removed from links. + /// + public static HtmlSnapshotOptions Section() => + Body() + .Select("section#elastic-docs-v3") + .Transform(root => + { + if (root.FirstElementChild is { TagName: "H1" } h1) + h1.Remove(); + }); +} diff --git a/tests-shared/Elastic.Documentation.Testing/Elastic.Documentation.Testing.csproj b/tests-shared/Elastic.Documentation.Testing/Elastic.Documentation.Testing.csproj new file mode 100644 index 0000000000..ab56af73cd --- /dev/null +++ b/tests-shared/Elastic.Documentation.Testing/Elastic.Documentation.Testing.csproj @@ -0,0 +1,28 @@ + + + + net10.0 + Elastic.Documentation.Testing + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests-shared/Elastic.Documentation.Testing/Linked/SnapshotScrubbing.cs b/tests-shared/Elastic.Documentation.Testing/Linked/SnapshotScrubbing.cs new file mode 100644 index 0000000000..6a80bb71fc --- /dev/null +++ b/tests-shared/Elastic.Documentation.Testing/Linked/SnapshotScrubbing.cs @@ -0,0 +1,39 @@ +// 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.Runtime.CompilerServices; +using System.Text.RegularExpressions; +using Nullean.Snapshots; + +namespace Elastic.Documentation.Testing.Linked; + +/// +/// Scrubbers every snapshot in docs-builder's tests gets: machine-specific paths and git SHAs never belong in a +/// verified file. Runs when any test project referencing this assembly loads it. +/// +internal static class SnapshotScrubbing +{ +#pragma warning disable CA2255 // compiled into each test project, which is application code + [ModuleInitializer] +#pragma warning restore CA2255 + internal static void Register() + { + SnapshotDefaults.AddScrubber( + Scrubbers.Regex( + new Regex(Regex.Escape(Path.GetTempPath().TrimEnd(Path.DirectorySeparatorChar)) + @"[^\s""'<>]*", RegexOptions.Compiled), + "{temp}" + ) + ); + if (FindSolutionRoot() is { } root) + SnapshotDefaults.AddScrubber(Scrubbers.Regex(new Regex(Regex.Escape(root), RegexOptions.Compiled), "{repo}")); + } + + private static string? FindSolutionRoot() + { + var dir = new DirectoryInfo(AppContext.BaseDirectory); + while (dir is not null && !File.Exists(Path.Combine(dir.FullName, "docs-builder.slnx"))) + dir = dir.Parent; + return dir?.FullName.TrimEnd(Path.DirectorySeparatorChar); + } +} diff --git a/tests-shared/Elastic.Documentation.Testing/MockFileSystemDump.cs b/tests-shared/Elastic.Documentation.Testing/MockFileSystemDump.cs new file mode 100644 index 0000000000..daaf2d8ed6 --- /dev/null +++ b/tests-shared/Elastic.Documentation.Testing/MockFileSystemDump.cs @@ -0,0 +1,35 @@ +// 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.IO.Abstractions; +using System.Text; + +namespace Elastic.Documentation.Testing; + +public static class MockFileSystemDump +{ + /// + /// Every file under , sorted by path, each under an === relative/path === + /// header. Meant for tests that read generated files back from a mock file system. + /// + public static string ToText(IFileSystem fileSystem, string directory, string searchPattern = "*") + { + var root = fileSystem.Path.GetFullPath(directory); + var sb = new StringBuilder(); + var files = fileSystem + .Directory + .EnumerateFiles(root, searchPattern, SearchOption.AllDirectories) + .Select(f => (Full: f, Relative: fileSystem.Path.GetRelativePath(root, f).Replace('\\', '/'))) + .OrderBy(f => f.Relative, StringComparer.Ordinal); + foreach (var (full, relative) in files) + { + sb.Append("=== ").Append(relative).Append(" ===\n"); + var content = fileSystem.File.ReadAllText(full).Replace("\r\n", "\n"); + sb.Append(content); + if (!content.EndsWith('\n')) + sb.Append('\n'); + } + return sb.ToString(); + } +} diff --git a/tests-shared/Elastic.Documentation.Testing/NavigationDump.cs b/tests-shared/Elastic.Documentation.Testing/NavigationDump.cs new file mode 100644 index 0000000000..a70dfa66f6 --- /dev/null +++ b/tests-shared/Elastic.Documentation.Testing/NavigationDump.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.Navigation; + +namespace Elastic.Documentation.Testing; + +public static class NavigationDump +{ + /// + /// The navigation tree as indented text, one node per line: Type url "title", with [hidden] and + /// [island] flags. A node lists its index page first (index: Type url "title"), then its children. The + /// index page is where most of the interesting URLs live. A child whose Parent is not the node it is listed + /// under is flagged [wrong parent]; index pages are not checked, because a virtual file's index has a + /// different parent. + /// + public static string ToText(INavigationItem root) + { + var sb = new StringBuilder(); + Write(sb, root, 0, null, null); + return sb.ToString(); + } + + private static void Write(StringBuilder sb, INavigationItem item, int depth, string? label, INavigationItem? container) + { + sb + .Append(' ', depth * 2) + .Append(label) + .Append(item.GetType().Name.Split('`')[0]) + .Append(" ") + .Append(item.Url) + .Append(" \"") + .Append(item.NavigationTitle) + .Append('"'); + if (item.Hidden) + sb.Append(" [hidden]"); + if (item.IsIsland) + sb.Append(" [island]"); + if (container is not null && !ReferenceEquals(item.Parent, container)) + sb.Append(" [wrong parent]"); + sb.Append('\n'); + + if (item is not INodeNavigationItem node) + return; + Write(sb, node.Index, depth + 1, "index: ", null); + foreach (var child in node.NavigationItems) + Write(sb, child, depth + 1, null, item); + } +} diff --git a/tests/Directory.Build.props b/tests/Directory.Build.props index 6ce509c511..b3667da5e1 100644 --- a/tests/Directory.Build.props +++ b/tests/Directory.Build.props @@ -27,5 +27,12 @@ + + + + + + + diff --git a/tests/Elastic.ApiExplorer.Tests/Elastic.ApiExplorer.Tests.csproj b/tests/Elastic.ApiExplorer.Tests/Elastic.ApiExplorer.Tests.csproj index b6138fd950..17b5bb72f8 100644 --- a/tests/Elastic.ApiExplorer.Tests/Elastic.ApiExplorer.Tests.csproj +++ b/tests/Elastic.ApiExplorer.Tests/Elastic.ApiExplorer.Tests.csproj @@ -12,8 +12,6 @@ - - diff --git a/tests/Elastic.Documentation.Tests/Testing/SnapshotHelpersTests.cs b/tests/Elastic.Documentation.Tests/Testing/SnapshotHelpersTests.cs new file mode 100644 index 0000000000..5897c67b4a --- /dev/null +++ b/tests/Elastic.Documentation.Tests/Testing/SnapshotHelpersTests.cs @@ -0,0 +1,70 @@ +// 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.IO.Abstractions.TestingHelpers; +using AwesomeAssertions; +using Elastic.Documentation.Diagnostics; +using Elastic.Documentation.Testing; +using Nullean.Snapshots; +using Nullean.Snapshots.Html; + +namespace Elastic.Documentation.Tests.Testing; + +public class SnapshotHelpersTests +{ + [Test] + public void ToSnapshotText_NoDiagnostics_IsEmpty() => new List().ToSnapshotText().Should().MatchInlineSnapshot(""); + + [Test] + public void ToSnapshotText_MixedSeverities_OneLineEachInOrder() + { + var diagnostics = new List + { + new() { Severity = Severity.Error, File = "a.md", Line = 3, Message = "bad link" }, + new() { Severity = Severity.Warning, File = "b.md", Message = "no line" }, + new() { Severity = Severity.Hint, File = "c.md", Line = 1, Message = "consider" } + }; + diagnostics + .ToSnapshotText() + .Should() + .MatchInlineSnapshot( + """ + error a.md:3 bad link + warning b.md no line + hint c.md:1 consider + """ + ); + } + + [Test] + public void MockFileSystemDump_SortsByPathAndNormalisesLineEndings() + { + var fs = new MockFileSystem(); + fs.AddFile("/out/b/two.md", new MockFileData("two\r\nlines")); + fs.AddFile("/out/a.md", new MockFileData("one\n")); + MockFileSystemDump + .ToText(fs, "/out") + .Should() + .MatchInlineSnapshot( + """ + === a.md === + one + === b/two.md === + two + lines + """ + ); + } + + [Test] + public void DocsHtmlSection_DropsFixtureHeadingAndHtmxAttributes() => + """ +

Test Document

x

nav
+ """.Should().MatchInlineHtmlSnapshot( + """ +

x

+ """, + DocsHtml.Section() + ); +} From a9fd42330390df7a7c5f9ced0b4102e1d1f7f9fa Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Thu, 5 Nov 2026 12:29:06 +0100 Subject: [PATCH 02/10] Compare rendered HTML with Nullean.Snapshots.Html ShouldBeHtml and ShouldContainHtml (Elastic.Markdown.Tests) and ConvertsToHtml, ConvertsToContainingHtml and the per-file ToHtml and ContainsHtml (Elastic.Authoring.Tests) keep their names and call sites, and now run on BeHtml and ContainHtml with the shared DocsHtml options. That removes the second copy of the AngleSharp.Diffing diff code and the AngleSharp.Diffing package. DiffPlex stays in Authoring for the plain text assertions. One test passed without testing anything. "marks exactly two options as a fork" gave the bare text `is-fork` to ConvertsToContainingHtml. The old normaliser printed only element children, so a text-only fragment became an empty string and every page contains an empty string. It now uses ConvertsToContainingRawHtml, like the test above it. Checked by corrupting one ShouldContainHtml and one ConvertsToHtml expectation: both fail. Co-Authored-By: Claude Sonnet 5.5 --- Directory.Packages.props | 1 - .../Blocks/Hub/GetStarted.cs | 2 +- .../Elastic.Authoring.Tests.csproj | 1 - .../Framework/HtmlAssertions.cs | 177 ++---------------- .../Elastic.Markdown.Tests.csproj | 2 - .../PrettyHtmlExtensions.cs | 127 +------------ 6 files changed, 22 insertions(+), 288 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index b1dcf0d2c0..0fdb1d5ee6 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -126,7 +126,6 @@
- diff --git a/tests/Elastic.Authoring.Tests/Blocks/Hub/GetStarted.cs b/tests/Elastic.Authoring.Tests/Blocks/Hub/GetStarted.cs index dec67a93fb..d0ccf282e0 100644 --- a/tests/Elastic.Authoring.Tests/Blocks/Hub/GetStarted.cs +++ b/tests/Elastic.Authoring.Tests/Blocks/Hub/GetStarted.cs @@ -164,7 +164,7 @@ public async Task PutsTheFirstStepInItsOwnBox() => await Docs.ConvertsToContainingRawHtml("""
  • """); [Test, DisplayName("marks exactly two options as a fork")] - public async Task MarksExactlyTwoOptionsAsAFork() => await Docs.ConvertsToContainingHtml("""is-fork"""); + public async Task MarksExactlyTwoOptionsAsAFork() => await Docs.ConvertsToContainingRawHtml("""is-fork"""); [Test, DisplayName("has no errors")] public async Task HasNoErrors() => await Docs.HasNoErrors(); diff --git a/tests/Elastic.Authoring.Tests/Elastic.Authoring.Tests.csproj b/tests/Elastic.Authoring.Tests/Elastic.Authoring.Tests.csproj index ee73b53ebd..4e5e05627f 100644 --- a/tests/Elastic.Authoring.Tests/Elastic.Authoring.Tests.csproj +++ b/tests/Elastic.Authoring.Tests/Elastic.Authoring.Tests.csproj @@ -5,7 +5,6 @@ - diff --git a/tests/Elastic.Authoring.Tests/Framework/HtmlAssertions.cs b/tests/Elastic.Authoring.Tests/Framework/HtmlAssertions.cs index 2bf599f62b..7fbebbdf03 100644 --- a/tests/Elastic.Authoring.Tests/Framework/HtmlAssertions.cs +++ b/tests/Elastic.Authoring.Tests/Framework/HtmlAssertions.cs @@ -3,13 +3,12 @@ // See the LICENSE file in the project root for more information using System.Diagnostics; -using AngleSharp.Diffing; -using AngleSharp.Diffing.Core; -using AngleSharp.Html; -using AngleSharp.Html.Parser; +using AwesomeAssertions; using DiffPlex.DiffBuilder; using DiffPlex.DiffBuilder.Model; +using Elastic.Documentation.Testing; using JetBrains.Annotations; +using Nullean.Snapshots.Html; namespace Elastic.Authoring.Tests.Framework; @@ -27,8 +26,7 @@ namespace Elastic.Authoring.Tests.Framework; /// would appear verbatim in every assertion snapshot otherwise. /// /// -/// Produces a humanized diff via that names the diff type -/// (NodeDiff / AttrDiff / MissingNodeDiff etc.) and shows expected vs. actual node text. +/// Compares through Nullean.Snapshots.Html, which reports a structural diff and then a line diff. /// /// /// All assertion methods are synchronous — the async wrapper lives on . @@ -73,173 +71,28 @@ internal static string Diff(string expected, string actual) } // ──────────────────────────────────────────────────────────────────────────────── - // AngleSharp deep HTML diff + // HTML comparison on Nullean.Snapshots.Html // ──────────────────────────────────────────────────────────────────────────────── - internal static string HtmlDiffString(IEnumerable diffs) - { - var formatter = new PrettyMarkupFormatter(); - - string NodeText(ComparisonSource source) - { - using var sw = new StringWriter(); - source.Node.ToHtml(sw, formatter); - return sw.ToString(); - } - - string AttrText(AttributeComparisonSource source) - { - using var sw = new StringWriter(); - source.Attribute.ToHtml(sw, formatter); - return sw.ToString(); - } - - // NodeDiffText and AttrDiffText accept non-nullable (callers pass .Control / .Test directly). - // For Missing*/Unexpected* cases we pass a sentinel "missing" string instead of calling these helpers. - string NodeDiffText(ComparisonSource control, ComparisonSource test) => - $"\nexpected: {NodeText(control)}\nactual: {NodeText(test)}\n"; - - string MissingNodeText(ComparisonSource control) => $"\nexpected: {NodeText(control)}\nactual: missing\n"; - - string UnexpectedNodeText(ComparisonSource test) => $"\nexpected: missing\nactual: {NodeText(test)}\n"; - - string AttrDiffText(AttributeComparisonSource control, AttributeComparisonSource test) => - $"\nexpected: {AttrText(control)}\nactual: {AttrText(test)}\n"; - - string MissingAttrText(AttributeComparisonSource control) => $"\nexpected: {AttrText(control)}\nactual: missing\n"; - - string UnexpectedAttrText(AttributeComparisonSource test) => $"\nexpected: missing\nactual: {AttrText(test)}\n"; - - static string NodeName(ComparisonSource source) => source.Node.NodeType.ToString().ToLowerInvariant(); - - return string.Join('\n', diffs.Select(d => - { - var label = d switch - { - NodeDiff nd when nd.Target == DiffTarget.Text && nd.Control.Path.Equals(nd.Test.Path, StringComparison.Ordinal) => - $"The text in {nd.Control.Path} is different.", - NodeDiff nd when nd.Target == DiffTarget.Text => - $"The expected {NodeName(nd.Control)} at {nd.Control.Path} and the actual {NodeName(nd.Test)} at {nd.Test.Path} is different.", - NodeDiff nd when nd.Control.Path.Equals(nd.Test.Path, StringComparison.Ordinal) => - $"The {NodeName(nd.Control)}s at {nd.Control.Path} are different.", - NodeDiff nd => - $"The expected {NodeName(nd.Control)} at {nd.Control.Path} and the actual {NodeName(nd.Test)} at {nd.Test.Path} are different.", - AttrDiff ad when ad.Control.Path.Equals(ad.Test.Path, StringComparison.Ordinal) => - $"The values of the attributes at {ad.Control.Path} are different.", - AttrDiff ad => $"The value of the attribute {ad.Control.Path} and actual attribute {ad.Test.Path} are different.", - MissingNodeDiff md => $"The {NodeName(md.Control)} at {md.Control.Path} is missing.", - MissingAttrDiff ma => $"The attribute at {ma.Control.Path} is missing.", - UnexpectedNodeDiff ud => $"The {NodeName(ud.Test)} at {ud.Test.Path} was not expected.", - UnexpectedAttrDiff ua => $"The attribute at {ua.Test.Path} was not expected.", - _ => throw new InvalidOperationException($"Unknown diff type detected: {d.GetType()}") - }; - - var detail = d switch - { - NodeDiff nd => NodeDiffText(nd.Control, nd.Test), - AttrDiff ad => AttrDiffText(ad.Control, ad.Test), - MissingNodeDiff md => MissingNodeText(md.Control), - MissingAttrDiff ma => MissingAttrText(ma.Control), - UnexpectedNodeDiff ud => UnexpectedNodeText(ud.Test), - UnexpectedAttrDiff ua => UnexpectedAttrText(ua.Test), - _ => throw new InvalidOperationException($"Unknown diff type detected: {d.GetType()}") - }; - - return label + detail; - })); - } - - // ──────────────────────────────────────────────────────────────────────────────── - // Pretty printer — matches the F# prettyHtml behaviour exactly - // ──────────────────────────────────────────────────────────────────────────────── - - private static string PrettyHtml(string html, string? querySelector) - { - var parser = new HtmlParser(); - var document = parser.ParseDocument(html); - var element = querySelector is null ? document.Body! : document.QuerySelector(querySelector)!; - - // Strip HTMX attributes from links — they are navigation artifacts, not content. - foreach (var link in element.QuerySelectorAll("a")) - { - link.RemoveAttribute("hx-get"); - link.RemoveAttribute("hx-select-oob"); - link.RemoveAttribute("hx-swap"); - link.RemoveAttribute("hx-indicator"); - link.RemoveAttribute("hx-push-url"); - link.RemoveAttribute("preload"); - } - - var formatter = new PrettyMarkupFormatter(); - using var sw = new StringWriter(); - - // Skip the first child if it is an H1 — the fixture wraps Setup.Markdown content in one. - var children = element.Children.ToList(); - for (var i = 0; i < children.Count; i++) - { - if (i == 0 && children[i].TagName == "H1") - continue; - children[i].ToHtml(sw, formatter); - } - - return sw.ToString().TrimStart('\n'); - } - - private static void CreateDiff(string expected, string actual) - { - // Identical pretty-printed HTML has no structural differences, so skip the AngleSharp comparison for the usual passing case. - if (string.Equals(expected, actual, StringComparison.Ordinal)) - return; - - var diffs = DiffBuilder.Compare(expected).WithTest(actual).Build(); - - var deepComparison = HtmlDiffString(diffs); - if (string.IsNullOrEmpty(deepComparison)) - return; - - var textDiff = Diff(expected, actual); - throw new AwesomeAssertions.Execution.AssertionFailedException( - $""" -Html was not equal --- DIFF -- -{textDiff} + /// + /// The rendered page's content section, minus the fixture's leading H1 and the htmx attributes on links. + /// The expected fragment is wrapped in the same section so both sides go through identical normalisation. + /// + private static HtmlSnapshotOptions Options => DocsHtml.Section(); --- Comparison -- -{deepComparison} -""" - ); - } + private static string InSection(string fragment) => $"
    {fragment}
    "; // ──────────────────────────────────────────────────────────────────────────────── // Per-file assertions (called from MarkdownResultTask / Scenario) // ──────────────────────────────────────────────────────────────────────────────── [DebuggerStepThrough] - internal static void ToHtml([LanguageInjection("html")] string expected, MarkdownResult actual) - { - var expectedHtml = PrettyHtml(expected, null); - var actualHtml = PrettyHtml(actual.Html, "section#elastic-docs-v3"); - CreateDiff(expectedHtml, actualHtml); - } + internal static void ToHtml([LanguageInjection("html")] string expected, MarkdownResult actual) => + actual.Html.Should().BeHtml(InSection(expected), Options); [DebuggerStepThrough] - internal static void ContainsHtml([LanguageInjection("html")] string expected, MarkdownResult actual) - { - var prettyExpected = PrettyHtml(expected, null); - var prettyActual = PrettyHtml(actual.Html, "section#elastic-docs-v3"); - - if (!prettyActual.Contains(prettyExpected, StringComparison.Ordinal)) - throw new AwesomeAssertions.Execution.AssertionFailedException( - $""" -Expected html to contain: -{prettyExpected} - -But was not found in: - -{prettyActual} -""" - ); - } + internal static void ContainsHtml([LanguageInjection("html")] string expected, MarkdownResult actual) => + actual.Html.Should().ContainHtml(InSection(expected), Options); [DebuggerStepThrough] internal static void ContainsRawHtml(string expected, MarkdownResult actual) diff --git a/tests/Elastic.Markdown.Tests/Elastic.Markdown.Tests.csproj b/tests/Elastic.Markdown.Tests/Elastic.Markdown.Tests.csproj index a128c14af7..e791376c2a 100644 --- a/tests/Elastic.Markdown.Tests/Elastic.Markdown.Tests.csproj +++ b/tests/Elastic.Markdown.Tests/Elastic.Markdown.Tests.csproj @@ -5,8 +5,6 @@ - - diff --git a/tests/Elastic.Markdown.Tests/PrettyHtmlExtensions.cs b/tests/Elastic.Markdown.Tests/PrettyHtmlExtensions.cs index eb552208b7..29cd3b7467 100644 --- a/tests/Elastic.Markdown.Tests/PrettyHtmlExtensions.cs +++ b/tests/Elastic.Markdown.Tests/PrettyHtmlExtensions.cs @@ -2,142 +2,27 @@ // 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 AngleSharp.Diffing; -using AngleSharp.Html; -using AngleSharp.Html.Parser; using AwesomeAssertions; -using AwesomeAssertions.Execution; -using DiffPlex.DiffBuilder; -using DiffPlex.DiffBuilder.Model; +using Elastic.Documentation.Testing; using JetBrains.Annotations; +using Nullean.Snapshots.Html; namespace Elastic.Markdown.Tests; +/// HTML comparisons on Nullean.Snapshots.Html: structural, so attribute order and whitespace never fail a test. public static class PrettyHtmlExtensions { - public static string PrettyHtml([LanguageInjection("html")] this string html, bool sanitize = true) - { - var parser = new HtmlParser(); - - var document = parser.ParseDocument(html); - var element = document.Body; - if (element is null) - return string.Empty; - - if (sanitize) - { - var links = element.QuerySelectorAll("a"); - foreach (var l in links) - { - l.RemoveAttribute("hx-get"); - l.RemoveAttribute("hx-select-oob"); - l.RemoveAttribute("hx-swap"); - l.RemoveAttribute("hx-indicator"); - l.RemoveAttribute("hx-push-url"); - l.RemoveAttribute("preload"); - } - } - - using var sw = new StringWriter(); - var formatter = new PrettyMarkupFormatter(); - foreach (var c in element.Children) - { - // ReSharper disable once AccessToDisposedClosure - c.ToHtml(sw, formatter); - } - return sw.ToString().TrimStart('\n'); - } + private static HtmlSnapshotOptions Options(bool sanitize) => sanitize ? DocsHtml.Body() : new HtmlSnapshotOptions(); public static void ShouldBeHtml( [LanguageInjection("html")] this string actual, [LanguageInjection("html")] string expected, bool sanitize = true - ) - { - expected = expected.Trim('\n').PrettyHtml(sanitize); - actual = actual.Trim('\n').PrettyHtml(sanitize); + ) => actual.Trim('\n').Should().BeHtml(expected.Trim('\n'), Options(sanitize)); - var diff = DiffBuilder.Compare(actual).WithTest(expected).Build().ToArray(); - - if (diff.Length == 0) - return; - - throw new AssertionFailedException(CreateDiff(actual, expected, sanitize)); - } public static void ShouldContainHtml( [LanguageInjection("html")] this string actual, [LanguageInjection("html")] string expected, bool sanitize = true - ) - { - expected = expected.Trim('\n').PrettyHtml(sanitize); - actual = actual.Trim('\n').PrettyHtml(sanitize); - - var actualCompare = actual.Replace("\t", string.Empty); - var expectedCompare = expected.Replace("\t", string.Empty); - - // we compare over unindented HTML, but if that fails, we rely on the pretty HTML Contain(). - // to throw for improved error messages - if (!actualCompare.Contains(expectedCompare)) - actual.Should().Contain(expected); - } - - public static string CreateDiff(this string actual, string expected, bool sanitize = true) - { - expected = expected.Trim('\n').PrettyHtml(sanitize); - actual = actual.Trim('\n').PrettyHtml(sanitize); - var diffLines = InlineDiffBuilder.Diff(expected, actual).Lines; - - var mutatedCount = diffLines.Count( - l => - l.Type switch - { - ChangeType.Unchanged => false, - ChangeType.Deleted => true, - ChangeType.Inserted => true, - ChangeType.Imaginary => false, - ChangeType.Modified => true, - _ => false - } - ); - if (mutatedCount == 0) - return string.Empty; - - var actualLineLength = actual.Split("\n").Length; - if (mutatedCount >= actualLineLength) - { - return $$""" - Mutations {{mutatedCount}} on all {{actualLineLength}} showing - EXPECTED: - {{expected}} - ACTUAL: - {{actual}} - """; - } - - using var sw = new StringWriter(); - foreach (var l in diffLines) - { - switch (l.Type) - { - case ChangeType.Unchanged: - sw.WriteLine(l.Text); - break; - case ChangeType.Deleted: - sw.WriteLine("- " + l.Text); - break; - case ChangeType.Inserted: - sw.WriteLine("+ " + l.Text); - break; - case ChangeType.Imaginary: - sw.WriteLine("? " + l.Text); - break; - case ChangeType.Modified: - sw.WriteLine("+ " + l.Text); - break; - } - } - - return sw.ToString(); - } + ) => actual.Trim('\n').Should().ContainHtml(expected.Trim('\n'), Options(sanitize)); } From 1ecc88d445fab7fef16dd019276ea8fd33a4769a Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Thu, 5 Nov 2026 12:38:35 +0100 Subject: [PATCH 03/10] Navigation tests: snapshot navigation trees instead of walking them The isolated and assembler navigation tests found nodes with ElementAt/First and asserted Url, type and child count one node at a time, so a wrong URL elsewhere in the tree went unnoticed (several only checked StartWith on a prefix). They now snapshot the whole tree inline as indented text. The shared NavigationDump leaves out each node's index page, where most URLs live, so NavigationTreeDump in this project adds an index line per node and flags a child whose Parent is not the node it is listed under. These tests became stricter: every URL, title, type, hidden/island flag and child count of the tree is now pinned, which includes every fact the removed assertions checked. Tests about behaviour (HomeProvider changes, island rendering, parent and root identity) are untouched. Co-Authored-By: Claude Sonnet 5.5 --- .../Assembler/ComplexSiteNavigationTests.cs | 315 +++++++---------- .../Assembler/SectionNavigationTests.cs | 35 +- .../Assembler/SiteDocumentationSetsTests.cs | 323 ++++++++++++------ .../Assembler/SiteNavigationTests.cs | 58 +++- .../Isolation/DeepLinkedIndexFileTests.cs | 95 ++++-- .../Isolation/DynamicUrlTests.cs | 150 ++++---- .../Isolation/FileNavigationTests.cs | 115 ++++--- .../Isolation/FolderIndexFileRefTests.cs | 20 +- .../Isolation/NavigationStructureTests.cs | 103 ++---- .../Navigation.Tests/Navigation.Tests.csproj | 5 + tests/Navigation.Tests/NavigationTreeDump.cs | 49 +++ 11 files changed, 731 insertions(+), 537 deletions(-) create mode 100644 tests/Navigation.Tests/NavigationTreeDump.cs diff --git a/tests/Navigation.Tests/Assembler/ComplexSiteNavigationTests.cs b/tests/Navigation.Tests/Assembler/ComplexSiteNavigationTests.cs index 86db964fe2..2f12c6343d 100644 --- a/tests/Navigation.Tests/Assembler/ComplexSiteNavigationTests.cs +++ b/tests/Navigation.Tests/Assembler/ComplexSiteNavigationTests.cs @@ -124,52 +124,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"); + NavigationTreeDump + .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 +212,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"); + NavigationTreeDump + .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 +271,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"); + NavigationTreeDump + .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 +331,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; + NavigationTreeDump + .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..ac5919ae78 100644 --- a/tests/Navigation.Tests/Assembler/SectionNavigationTests.cs +++ b/tests/Navigation.Tests/Assembler/SectionNavigationTests.cs @@ -77,11 +77,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); + NavigationTreeDump + .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..bd0b086f12 100644 --- a/tests/Navigation.Tests/Assembler/SiteDocumentationSetsTests.cs +++ b/tests/Navigation.Tests/Assembler/SiteDocumentationSetsTests.cs @@ -124,18 +124,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"); + NavigationTreeDump + .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 +205,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"); + NavigationTreeDump + .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 +284,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"); + NavigationTreeDump + .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 +364,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 + NavigationTreeDump + .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 +401,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 + NavigationTreeDump + .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 +441,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 + NavigationTreeDump + .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 +492,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"); - } + NavigationTreeDump + .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 +546,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"); + NavigationTreeDump + .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..2048ba6186 100644 --- a/tests/Navigation.Tests/Assembler/SiteNavigationTests.cs +++ b/tests/Navigation.Tests/Assembler/SiteNavigationTests.cs @@ -72,10 +72,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); + NavigationTreeDump + .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 +139,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(); + NavigationTreeDump + .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/Isolation/DeepLinkedIndexFileTests.cs b/tests/Navigation.Tests/Isolation/DeepLinkedIndexFileTests.cs index e5b0c94efc..4509491035 100644 --- a/tests/Navigation.Tests/Isolation/DeepLinkedIndexFileTests.cs +++ b/tests/Navigation.Tests/Isolation/DeepLinkedIndexFileTests.cs @@ -107,10 +107,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(); + NavigationTreeDump + .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 +148,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) + NavigationTreeDump + .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 +193,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"); + NavigationTreeDump + .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 +239,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"); + NavigationTreeDump + .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 +281,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"); + NavigationTreeDump + .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..144b4e35b3 100644 --- a/tests/Navigation.Tests/Isolation/DynamicUrlTests.cs +++ b/tests/Navigation.Tests/Isolation/DynamicUrlTests.cs @@ -110,10 +110,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"); + NavigationTreeDump + .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 +144,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(); + NavigationTreeDump + .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 +177,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(); + NavigationTreeDump + .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 +211,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); + NavigationTreeDump + .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 +248,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); + NavigationTreeDump + .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 +285,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); + NavigationTreeDump + .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 +321,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"); + NavigationTreeDump + .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..5426e3dd12 100644 --- a/tests/Navigation.Tests/Isolation/FileNavigationTests.cs +++ b/tests/Navigation.Tests/Isolation/FileNavigationTests.cs @@ -32,9 +32,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"); + NavigationTreeDump + .ToText(navigation) + .Should() + .MatchInlineSnapshot( + """ + DocumentationSetNavigation /getting-started "getting-started" + index: FileNavigationLeaf /getting-started "getting-started" + """ + ); } [Test] @@ -58,18 +64,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); + NavigationTreeDump + .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 +100,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); + NavigationTreeDump + .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 +137,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); + NavigationTreeDump + .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 +210,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"); + NavigationTreeDump + .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..47dbb0567c 100644 --- a/tests/Navigation.Tests/Isolation/FolderIndexFileRefTests.cs +++ b/tests/Navigation.Tests/Isolation/FolderIndexFileRefTests.cs @@ -40,14 +40,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 + NavigationTreeDump + .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..7dcd0de3d1 100644 --- a/tests/Navigation.Tests/Isolation/NavigationStructureTests.cs +++ b/tests/Navigation.Tests/Isolation/NavigationStructureTests.cs @@ -170,54 +170,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) + NavigationTreeDump + .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 +238,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 + NavigationTreeDump + .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..2f8abb8b18 100644 --- a/tests/Navigation.Tests/Navigation.Tests.csproj +++ b/tests/Navigation.Tests/Navigation.Tests.csproj @@ -19,6 +19,11 @@
    + + + + + diff --git a/tests/Navigation.Tests/NavigationTreeDump.cs b/tests/Navigation.Tests/NavigationTreeDump.cs new file mode 100644 index 0000000000..20bd4f3939 --- /dev/null +++ b/tests/Navigation.Tests/NavigationTreeDump.cs @@ -0,0 +1,49 @@ +// 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; + +namespace Elastic.Documentation.Navigation.Tests; + +/// +/// Like Elastic.Documentation.Testing.NavigationDump, but also prints each node's index page +/// (index: Type url "title") before its children, and flags a child whose Parent is not the node it is listed under. The index page is where most of the interesting +/// URLs live, and NavigationDump leaves it out. +/// +public static class NavigationTreeDump +{ + public static string ToText(INavigationItem root) + { + var sb = new StringBuilder(); + Write(sb, root, 0, null, null); + return sb.ToString(); + } + + private static void Write(StringBuilder sb, INavigationItem item, int depth, string? label, INavigationItem? container) + { + sb + .Append(' ', depth * 2) + .Append(label) + .Append(item.GetType().Name.Split('`')[0]) + .Append(" ") + .Append(item.Url) + .Append(" \"") + .Append(item.NavigationTitle) + .Append('"'); + if (item.Hidden) + sb.Append(" [hidden]"); + if (item.IsIsland) + sb.Append(" [island]"); + // every child points back at the node it is listed under + if (container is not null && !ReferenceEquals(item.Parent, container)) + sb.Append(" [wrong parent]"); + sb.Append('\n'); + + if (item is not INodeNavigationItem node) + return; + Write(sb, node.Index, depth + 1, "index: ", null); + foreach (var child in node.NavigationItems) + Write(sb, child, depth + 1, null, item); + } +} From 838c8dee3053ed54853d6f6dbae55fc3c43bcbcb Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Thu, 5 Nov 2026 12:43:39 +0100 Subject: [PATCH 04/10] Navigation rendering tests: snapshot the rendered component The secondary nav, nav tree, table of contents, footer, docs bar and nav side tests checked fragments with Contain/NotContain and IndexOf ordering. They now write the rendered component to a verified HTML file (htmx attributes on links removed), so an unexpected extra, missing or reordered element shows in the diff. These tests became stricter. Before accepting, every Contain/NotContain literal of the old assertions was checked against the snapshot text with a script (the scoped negatives, such as 'no chevron inside the page link' and 'no before the title', were checked by reading the HTML). Several tests rendered the same input and asserted different fragments of it (top nav bar: 5 tests into 1, built-in bar: 2 into 1, mobile drawer: 2 into 1, docs bar: 4 into 1), which would have produced identical snapshot files. They are merged into one test per render, so the test count drops by 8 with no coverage lost. Left as Contain checks: the landing page and style guide pages (whole pages of several hundred lines), the head asset ordering tests and the examples' generic invariants. Co-Authored-By: Claude Sonnet 5.5 --- ...emHeaderInsteadOfTheReactOne.verified.html | 172 +++++++++++++++ ...rnalSlotSearchAndThemeSwitch.verified.html | 206 ++++++++++++++++++ ...AndTheSectionTitleAsAHeading.verified.html | 100 +++++++++ ...BackLinks_KeepsTheQuietLabel.verified.html | 93 ++++++++ ...MarksAbsoluteLinksAsExternal.verified.html | 23 ++ ...bendaLinksAfterTrademarkCopy.verified.html | 51 +++++ ...heApiAndItsVersionsInOneMenu.verified.html | 51 +++++ ...atestSuffixButTheMenuKeepsIt.verified.html | 21 ++ ...VersionsOnlyListsTheVersions.verified.html | 20 ++ ...rsionsOrOtherApisIsPlainText.verified.html | 3 + ...edTopNavRendersTheDesktopBar.verified.html | 65 ++++++ ...inkOnlyTopNavRendersItsLinks.verified.html | 53 +++++ ...DrawerRendersVersionDropdown.verified.html | 20 ++ ...NavLinksRenderInMobileDrawer.verified.html | 34 +++ ...MobileDrawerIncludesDocsHome.verified.html | 34 +++ ...nDoesNotRenderOnTheLegacyBar.verified.html | 31 +++ ...opdownRendersOnTheBuiltInBar.verified.html | 50 +++++ ...RendersOnTheRightOfTheTopBar.verified.html | 70 ++++++ ...onTheBuiltInLinksAreRendered.verified.html | 45 ++++ ...agOff_RendersVersionDropdown.verified.html | 61 ++++++ ..._FlagOn_OmitsVersionDropdown.verified.html | 57 +++++ ...solated_OmitsVersionDropdown.verified.html | 22 ++ ...PageLinkAndTheToggleSeparate.verified.html | 26 +++ ...dingRow_IsALabelNotAPageLink.verified.html | 22 ++ ...endersHeadingAndOverviewLeaf.verified.html | 24 ++ ...sALinkWithoutAnExpandControl.verified.html | 15 ++ ...ndersTitleWithoutAnEmptyLink.verified.html | 32 +++ ...arator_KeepsAutomaticDivider.verified.html | 17 ++ ...endersTheDesignSystemSidebar.verified.html | 33 +++ ...ator_RendersAfterIntroLeaves.verified.html | 32 +++ ...onSwitcher_StaysOutOfTheTree.verified.html | 11 + .../DesignSystem/DocsBarRenderingTests.cs | 43 +--- .../DesignSystem/NavSideRenderingTests.cs | 9 +- .../Navigation.Tests/Navigation.Tests.csproj | 1 + .../Rendering/FooterRenderingTests.cs | 26 +-- .../Rendering/SecondaryNavRenderingTests.cs | 152 ++----------- .../TableOfContentsRenderingTests.cs | 13 +- .../Rendering/TocTreeRenderingTests.cs | 74 +------ 38 files changed, 1530 insertions(+), 282 deletions(-) create mode 100644 tests/Navigation.Tests/.snapshots/DesignSystem/DocsBarRenderingTests/AirGapped_UsesTheDesignSystemHeaderInsteadOfTheReactOne.verified.html create mode 100644 tests/Navigation.Tests/.snapshots/DesignSystem/DocsBarRenderingTests/DocsBar_RendersTheActiveSectionDropdownExternalSlotSearchAndThemeSwitch.verified.html create mode 100644 tests/Navigation.Tests/.snapshots/DesignSystem/NavSideRenderingTests/IslandSidebar_ShowsBackLinksAsPillsAndTheSectionTitleAsAHeading.verified.html create mode 100644 tests/Navigation.Tests/.snapshots/DesignSystem/NavSideRenderingTests/SectionSidebar_WithoutBackLinks_KeepsTheQuietLabel.verified.html create mode 100644 tests/Navigation.Tests/.snapshots/DesignSystem/NavSideRenderingTests/Tree_MarksAbsoluteLinksAsExternal.verified.html create mode 100644 tests/Navigation.Tests/.snapshots/Rendering/FooterRenderingTests/AssemblerFooter_RendersIubendaLinksAfterTrademarkCopy.verified.html create mode 100644 tests/Navigation.Tests/.snapshots/Rendering/SecondaryNavRenderingTests/ApiIdentityShowsTheApiAndItsVersionsInOneMenu.verified.html create mode 100644 tests/Navigation.Tests/.snapshots/Rendering/SecondaryNavRenderingTests/ApiIdentityTriggerDropsTheLatestSuffixButTheMenuKeepsIt.verified.html create mode 100644 tests/Navigation.Tests/.snapshots/Rendering/SecondaryNavRenderingTests/ApiIdentityWithVersionsOnlyListsTheVersions.verified.html create mode 100644 tests/Navigation.Tests/.snapshots/Rendering/SecondaryNavRenderingTests/ApiIdentityWithoutVersionsOrOtherApisIsPlainText.verified.html create mode 100644 tests/Navigation.Tests/.snapshots/Rendering/SecondaryNavRenderingTests/ConfiguredTopNavRendersTheDesktopBar.verified.html create mode 100644 tests/Navigation.Tests/.snapshots/Rendering/SecondaryNavRenderingTests/LinkOnlyTopNavRendersItsLinks.verified.html create mode 100644 tests/Navigation.Tests/.snapshots/Rendering/SecondaryNavRenderingTests/MobileDrawerRendersVersionDropdown.verified.html create mode 100644 tests/Navigation.Tests/.snapshots/Rendering/SecondaryNavRenderingTests/TopNavLinksRenderInMobileDrawer.verified.html create mode 100644 tests/Navigation.Tests/.snapshots/Rendering/SecondaryNavRenderingTests/TopNavMobileDrawerIncludesDocsHome.verified.html create mode 100644 tests/Navigation.Tests/.snapshots/Rendering/SecondaryNavRenderingTests/VersionDropdownDoesNotRenderOnTheLegacyBar.verified.html create mode 100644 tests/Navigation.Tests/.snapshots/Rendering/SecondaryNavRenderingTests/VersionDropdownRendersOnTheBuiltInBar.verified.html create mode 100644 tests/Navigation.Tests/.snapshots/Rendering/SecondaryNavRenderingTests/VersionDropdownRendersOnTheRightOfTheTopBar.verified.html create mode 100644 tests/Navigation.Tests/.snapshots/Rendering/SecondaryNavRenderingTests/WithoutConfigurationTheBuiltInLinksAreRendered.verified.html create mode 100644 tests/Navigation.Tests/.snapshots/Rendering/TableOfContentsRenderingTests/Assembler_FlagOff_RendersVersionDropdown.verified.html create mode 100644 tests/Navigation.Tests/.snapshots/Rendering/TableOfContentsRenderingTests/Assembler_FlagOn_OmitsVersionDropdown.verified.html create mode 100644 tests/Navigation.Tests/.snapshots/Rendering/TableOfContentsRenderingTests/Isolated_OmitsVersionDropdown.verified.html create mode 100644 tests/Navigation.Tests/.snapshots/Rendering/TocTreeRenderingTests/FolderRow_KeepsThePageLinkAndTheToggleSeparate.verified.html create mode 100644 tests/Navigation.Tests/.snapshots/Rendering/TocTreeRenderingTests/HeadingRow_IsALabelNotAPageLink.verified.html create mode 100644 tests/Navigation.Tests/.snapshots/Rendering/TocTreeRenderingTests/IslandOverview_RendersHeadingAndOverviewLeaf.verified.html create mode 100644 tests/Navigation.Tests/.snapshots/Rendering/TocTreeRenderingTests/IslandStub_IsALinkWithoutAnExpandControl.verified.html create mode 100644 tests/Navigation.Tests/.snapshots/Rendering/TocTreeRenderingTests/LegacyHeadingRow_RendersTitleWithoutAnEmptyLink.verified.html create mode 100644 tests/Navigation.Tests/.snapshots/Rendering/TocTreeRenderingTests/RootIndex_WithoutTreeSeparator_KeepsAutomaticDivider.verified.html create mode 100644 tests/Navigation.Tests/.snapshots/Rendering/TocTreeRenderingTests/TocTree_RendersTheDesignSystemSidebar.verified.html create mode 100644 tests/Navigation.Tests/.snapshots/Rendering/TocTreeRenderingTests/TreeSeparator_RendersAfterIntroLeaves.verified.html create mode 100644 tests/Navigation.Tests/.snapshots/Rendering/TocTreeRenderingTests/VersionSwitcher_StaysOutOfTheTree.verified.html 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/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/Navigation.Tests.csproj b/tests/Navigation.Tests/Navigation.Tests.csproj index 2f8abb8b18..11654a9b69 100644 --- a/tests/Navigation.Tests/Navigation.Tests.csproj +++ b/tests/Navigation.Tests/Navigation.Tests.csproj @@ -20,6 +20,7 @@ + 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("