From 62867c6e28548157ab8c60318591d3e26838dda8 Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Thu, 5 Nov 2026 12:26:24 +0100 Subject: [PATCH 1/8] 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 2/8] 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 7c4a36785338370bb70ac4e03cd08e0f31de5a5d Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Thu, 5 Nov 2026 12:34:51 +0100 Subject: [PATCH 3/8] Authoring tests: expected text, JSON and HTML as inline snapshots ConvertsToHtml, ToHtml, ConvertsToPlainText, ConvertsToNewLlm, ConvertsToLlmWithMetadata and ConvertsToJson keep their names and call sites, but now compare through Nullean.Snapshots inline snapshots, so the expected literal is accepted with snap instead of hand-edited. The wrappers forward the caller's file, member and line and name themselves as the method snap looks for in the source (InlineSnapshots.cs). The expected literals passed unchanged except for these, which snap rewrote: - LLM markdown literals started with a blank line that the old comparison trimmed; they are now the trimmed text. - Two ToHtml literals (IncludeBlocks) listed an

    that the old BeHtml stripped from both sides; the snapshot no longer has it. - The old text comparison ignored whitespace (DiffPlex default), so indentation mistakes in four literals went unnoticed: mermaid code lost its indent in two, and nested list items and a fenced block in a nested list lose their indentation in the exporter output. The snapshots record the output as it is today and the tests carry a note. These comparisons are therefore stricter than before. ConvertsToJson compares as a tree, so key order no longer matters. Co-Authored-By: Claude Sonnet 5.5 --- .../Directives/IncludeBlocks.cs | 14 +- .../Framework/HtmlAssertions.cs | 8 +- .../Framework/InlineSnapshots.cs | 36 ++ .../Framework/LlmMarkdownAssertions.cs | 61 +- .../Framework/PlainTextAssertions.cs | 23 +- .../Framework/ResultsAssertions.cs | 21 +- .../Framework/Setup.cs | 81 ++- .../LlmMarkdown/LlmMarkdownOutput.cs | 599 +++++++++--------- .../PlainText/PlainTextOutput.cs | 5 +- 9 files changed, 424 insertions(+), 424 deletions(-) create mode 100644 tests/Elastic.Authoring.Tests/Framework/InlineSnapshots.cs diff --git a/tests/Elastic.Authoring.Tests/Directives/IncludeBlocks.cs b/tests/Elastic.Authoring.Tests/Directives/IncludeBlocks.cs index 2f479072f9..fbafd37f04 100644 --- a/tests/Elastic.Authoring.Tests/Directives/IncludeBlocks.cs +++ b/tests/Elastic.Authoring.Tests/Directives/IncludeBlocks.cs @@ -36,9 +36,10 @@ [link to root with included anchor](index.md#aa) public async Task ValidateIndexHtml() => await Docs.Converts("index.md").ToHtml( """ -

    A Document that lives at the root

    """ ); @@ -47,11 +48,14 @@ await Docs.Converts("index.md").ToHtml( public async Task ValidateTestLinksHtml() => await Docs.Converts("test-links.md").ToHtml( """ -

    parent.md

    -

    link to root with included anchor

    +

    + link to root with included anchor +

    """ ); diff --git a/tests/Elastic.Authoring.Tests/Framework/HtmlAssertions.cs b/tests/Elastic.Authoring.Tests/Framework/HtmlAssertions.cs index 7fbebbdf03..59e3769812 100644 --- a/tests/Elastic.Authoring.Tests/Framework/HtmlAssertions.cs +++ b/tests/Elastic.Authoring.Tests/Framework/HtmlAssertions.cs @@ -87,8 +87,8 @@ internal static string Diff(string expected, string actual) // ──────────────────────────────────────────────────────────────────────────────── [DebuggerStepThrough] - internal static void ToHtml([LanguageInjection("html")] string expected, MarkdownResult actual) => - actual.Html.Should().BeHtml(InSection(expected), Options); + internal static void ToHtml([LanguageInjection("html")] string? expected, MarkdownResult actual, CallSite site) => + InlineSnapshots.Html(actual.Html, expected, nameof(MarkdownResultTask.ToHtml), site); [DebuggerStepThrough] internal static void ContainsHtml([LanguageInjection("html")] string expected, MarkdownResult actual) => @@ -137,8 +137,8 @@ private static MarkdownResult FindIndexMd(GeneratorResults results) => ?? throw new AwesomeAssertions.Execution.AssertionFailedException("Could not find 'index.md' in generator results"); [DebuggerStepThrough] - internal static void ConvertsToHtml([LanguageInjection("html")] string expected, GeneratorResults results) => - ToHtml(expected, FindIndexMd(results)); + internal static void ConvertsToHtml([LanguageInjection("html")] string? expected, GeneratorResults results, CallSite site) => + InlineSnapshots.Html(FindIndexMd(results).Html, expected, nameof(Scenario.ConvertsToHtml), site); [DebuggerStepThrough] internal static void ConvertsToContainingHtml([LanguageInjection("html")] string expected, GeneratorResults results) => diff --git a/tests/Elastic.Authoring.Tests/Framework/InlineSnapshots.cs b/tests/Elastic.Authoring.Tests/Framework/InlineSnapshots.cs new file mode 100644 index 0000000000..e3a24ab231 --- /dev/null +++ b/tests/Elastic.Authoring.Tests/Framework/InlineSnapshots.cs @@ -0,0 +1,36 @@ +// 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 AwesomeAssertions; +using Nullean.Snapshots; +using Nullean.Snapshots.Html; +using Nullean.Snapshots.Json; + +namespace Elastic.Authoring.Tests.Framework; + +/// +/// Where a or method was called from in a test. Inline snapshots are +/// rewritten at the call site, so a wrapper has to hand the compiler-filled caller information on to the library +/// and name itself (not MatchInlineSnapshot) as the method snap looks for in the source. +/// +internal readonly record struct CallSite(string File, string Member, int Line, string? Expression) +{ + internal InlineSnapshotCallSite For(string method) => new(new SnapshotCallSite(File, Member, Line), method, Expression); +} + +/// Inline snapshot assertions for the text a documentation page renders to, in each format the harness compares. +internal static class InlineSnapshots +{ + internal static void Text(string actual, string? expected, string method, CallSite site) => + Match(actual, expected, SnapshotFormats.Text, method, site); + + internal static void Html(string html, string? expected, string method, CallSite site) => + Match(HtmlSnapshot.Pretty(html, Documentation.Testing.DocsHtml.Section()), expected, HtmlSnapshot.Format, method, site); + + internal static void Json(string json, string? expected, string method, CallSite site) => + Match(JsonSnapshot.Pretty(json), expected, JsonSnapshot.Format, method, site); + + private static void Match(string actual, string? expected, SnapshotFormat format, string method, CallSite site) => + SnapshotAssertions.MatchInline(actual.Should(), actual, expected, format, site.For(method), null, string.Empty); +} diff --git a/tests/Elastic.Authoring.Tests/Framework/LlmMarkdownAssertions.cs b/tests/Elastic.Authoring.Tests/Framework/LlmMarkdownAssertions.cs index 06ad24f6a8..757995abee 100644 --- a/tests/Elastic.Authoring.Tests/Framework/LlmMarkdownAssertions.cs +++ b/tests/Elastic.Authoring.Tests/Framework/LlmMarkdownAssertions.cs @@ -75,58 +75,17 @@ internal static string ToLlmMarkdownWithMetadata(MarkdownResult actual) } [DebuggerStepThrough] - internal static void ConvertsToNewLlm([LanguageInjection("markdown")] string expected, GeneratorResults results) - { - var defaultFile = results.MarkdownResults.FirstOrDefault(r => r.File.RelativePath == "index.md") - ?? throw new AwesomeAssertions.Execution.AssertionFailedException("Could not find 'index.md' in generator results"); - - var actual = ToLlmMarkdown(defaultFile); - var expectedTrimmed = expected.Trim(); - var difference = HtmlAssertions.Diff(expectedTrimmed, actual); - - if (string.IsNullOrEmpty(difference)) - return; - - throw new AwesomeAssertions.Execution.AssertionFailedException( - $""" -LLM text was not equal --- DIFF -- -{difference} - --- EXPECTED -- -{expectedTrimmed} - --- ACTUAL -- -{actual} -""" - ); - } + internal static void ConvertsToNewLlm([LanguageInjection("markdown")] string? expected, GeneratorResults results, CallSite site) => + InlineSnapshots.Text(ToLlmMarkdown(FindIndexMd(results)), expected, nameof(Scenario.ConvertsToNewLlm), site); [DebuggerStepThrough] - internal static void ConvertsToLlmWithMetadata([LanguageInjection("markdown")] string expected, GeneratorResults results) - { - var defaultFile = results.MarkdownResults.FirstOrDefault(r => r.File.RelativePath == "index.md") + internal static void ConvertsToLlmWithMetadata( + [LanguageInjection("markdown")] string? expected, + GeneratorResults results, + CallSite site + ) => InlineSnapshots.Text(ToLlmMarkdownWithMetadata(FindIndexMd(results)), expected, nameof(Scenario.ConvertsToLlmWithMetadata), site); + + private static MarkdownResult FindIndexMd(GeneratorResults results) => + results.MarkdownResults.FirstOrDefault(r => r.File.RelativePath == "index.md") ?? throw new AwesomeAssertions.Execution.AssertionFailedException("Could not find 'index.md' in generator results"); - - var actual = ToLlmMarkdownWithMetadata(defaultFile); - var expectedTrimmed = expected.Trim(); - var difference = HtmlAssertions.Diff(expectedTrimmed, actual); - - if (string.IsNullOrEmpty(difference)) - return; - - throw new AwesomeAssertions.Execution.AssertionFailedException( - $""" -LLM metadata output was not equal --- DIFF -- -{difference} - --- EXPECTED -- -{expectedTrimmed} - --- ACTUAL -- -{actual} -""" - ); - } } diff --git a/tests/Elastic.Authoring.Tests/Framework/PlainTextAssertions.cs b/tests/Elastic.Authoring.Tests/Framework/PlainTextAssertions.cs index 5a42b44df4..1cb69056be 100644 --- a/tests/Elastic.Authoring.Tests/Framework/PlainTextAssertions.cs +++ b/tests/Elastic.Authoring.Tests/Framework/PlainTextAssertions.cs @@ -17,30 +17,11 @@ internal static string ToPlainText(MarkdownResult actual) => PlainTextExporter.ConvertToPlainText(actual.Document, actual.Context.Generator.Context).Trim(); [DebuggerStepThrough] - internal static void ConvertsToPlainText([LanguageInjection("text")] string expected, GeneratorResults results) + internal static void ConvertsToPlainText([LanguageInjection("text")] string? expected, GeneratorResults results, CallSite site) { var defaultFile = results.MarkdownResults.FirstOrDefault(r => r.File.RelativePath == "index.md") ?? throw new AwesomeAssertions.Execution.AssertionFailedException("Could not find 'index.md' in generator results"); - var actual = ToPlainText(defaultFile); - var expectedTrimmed = expected.Trim(); - var difference = HtmlAssertions.Diff(expectedTrimmed, actual); - - if (string.IsNullOrEmpty(difference)) - return; - - throw new AwesomeAssertions.Execution.AssertionFailedException( - $""" -Plain text was not equal --- DIFF -- -{difference} - --- EXPECTED -- -{expectedTrimmed} - --- ACTUAL -- -{actual} -""" - ); + InlineSnapshots.Text(ToPlainText(defaultFile), expected, nameof(Scenario.ConvertsToPlainText), site); } } diff --git a/tests/Elastic.Authoring.Tests/Framework/ResultsAssertions.cs b/tests/Elastic.Authoring.Tests/Framework/ResultsAssertions.cs index 42bd94bd60..e8d18b9904 100644 --- a/tests/Elastic.Authoring.Tests/Framework/ResultsAssertions.cs +++ b/tests/Elastic.Authoring.Tests/Framework/ResultsAssertions.cs @@ -3,7 +3,6 @@ // See the LICENSE file in the project root for more information using System.Diagnostics; -using System.Text.Json; using JetBrains.Annotations; namespace Elastic.Authoring.Tests.Framework; @@ -14,8 +13,6 @@ namespace Elastic.Authoring.Tests.Framework; /// internal static class ResultsAssertions { - private static readonly JsonSerializerOptions IndentedJson = new() { WriteIndented = true }; - /// /// Finds the for in the generator output. /// Mirrors F# converts. Called from . @@ -36,22 +33,18 @@ internal static MarkdownResult Finds(string path, GeneratorResults results) /// Port of F# convertsToJson. /// [DebuggerStepThrough] - internal static void ConvertsToJson(string artifactPath, [LanguageInjection("json")] string expected, GeneratorResults results) + internal static void ConvertsToJson( + string artifactPath, + [LanguageInjection("json")] string? expected, + GeneratorResults results, + CallSite site + ) { var fs = results.Context.ReadFileSystem; var fi = fs.FileInfo.New(artifactPath); if (!fi.Exists) throw new AwesomeAssertions.Execution.AssertionFailedException($"{artifactPath} is not part of the output"); - var actualRaw = fs.File.ReadAllText(fi.FullName); - using var actualDoc = JsonDocument.Parse(actualRaw); - var actualPretty = JsonSerializer.Serialize(actualDoc, IndentedJson); - - using var expectedDoc = JsonDocument.Parse(expected); - var expectedPretty = JsonSerializer.Serialize(expectedDoc, IndentedJson); - - var diff = HtmlAssertions.Diff(expectedPretty, actualPretty); - if (!string.IsNullOrEmpty(diff)) - throw new AwesomeAssertions.Execution.AssertionFailedException($"JSON was not equal\n-- DIFF --\n{diff}"); + InlineSnapshots.Json(fs.File.ReadAllText(fi.FullName), expected, nameof(Scenario.ConvertsToJson), site); } } diff --git a/tests/Elastic.Authoring.Tests/Framework/Setup.cs b/tests/Elastic.Authoring.Tests/Framework/Setup.cs index a489c72c34..161575a254 100644 --- a/tests/Elastic.Authoring.Tests/Framework/Setup.cs +++ b/tests/Elastic.Authoring.Tests/Framework/Setup.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information using System.Collections.Concurrent; +using System.Runtime.CompilerServices; using AwesomeAssertions; using JetBrains.Annotations; @@ -101,8 +102,18 @@ internal Scenario(Func> build) => /// Asserts that index.md renders to HTML that is structurally identical to /// (after pretty-printing both sides). /// - public async Task ConvertsToHtml([LanguageInjection("html")] string expected) => - HtmlAssertions.ConvertsToHtml(expected, await BuildAsync()); + public async Task ConvertsToHtml( + [LanguageInjection("html")] string? expected = null, + [CallerArgumentExpression(nameof(expected))] string? expectedExpression = null, + [CallerFilePath] string callerFilePath = "", + [CallerMemberName] string callerMemberName = "", + [CallerLineNumber] int callerLineNumber = 0 + ) => + HtmlAssertions.ConvertsToHtml( + expected, + await BuildAsync(), + new CallSite(callerFilePath, callerMemberName, callerLineNumber, expectedExpression) + ); /// /// Asserts that index.md's rendered HTML contains the @@ -193,8 +204,18 @@ public async Task AppliesTo(Elastic.Documentation.AppliesTo.ApplicableTo? expect /// Asserts that index.md's plain-text export equals /// (both sides trimmed; diff shown on mismatch). /// - public async Task ConvertsToPlainText([LanguageInjection("text")] string expected) => - PlainTextAssertions.ConvertsToPlainText(expected, await BuildAsync()); + public async Task ConvertsToPlainText( + [LanguageInjection("text")] string? expected = null, + [CallerArgumentExpression(nameof(expected))] string? expectedExpression = null, + [CallerFilePath] string callerFilePath = "", + [CallerMemberName] string callerMemberName = "", + [CallerLineNumber] int callerLineNumber = 0 + ) => + PlainTextAssertions.ConvertsToPlainText( + expected, + await BuildAsync(), + new CallSite(callerFilePath, callerMemberName, callerLineNumber, expectedExpression) + ); // ──────────────────────────────────────────────────────────────────────────────── // LLM Markdown @@ -204,15 +225,35 @@ public async Task ConvertsToPlainText([LanguageInjection("text")] string expecte /// Asserts that index.md's LLM Markdown export equals /// (both sides trimmed; diff shown on mismatch). /// - public async Task ConvertsToNewLlm([LanguageInjection("markdown")] string expected) => - LlmMarkdownAssertions.ConvertsToNewLlm(expected, await BuildAsync()); + public async Task ConvertsToNewLlm( + [LanguageInjection("markdown")] string? expected = null, + [CallerArgumentExpression(nameof(expected))] string? expectedExpression = null, + [CallerFilePath] string callerFilePath = "", + [CallerMemberName] string callerMemberName = "", + [CallerLineNumber] int callerLineNumber = 0 + ) => + LlmMarkdownAssertions.ConvertsToNewLlm( + expected, + await BuildAsync(), + new CallSite(callerFilePath, callerMemberName, callerLineNumber, expectedExpression) + ); /// /// Asserts that index.md's LLM Markdown output including front-matter metadata /// equals (see warning). /// - public async Task ConvertsToLlmWithMetadata([LanguageInjection("markdown")] string expected) => - LlmMarkdownAssertions.ConvertsToLlmWithMetadata(expected, await BuildAsync()); + public async Task ConvertsToLlmWithMetadata( + [LanguageInjection("markdown")] string? expected = null, + [CallerArgumentExpression(nameof(expected))] string? expectedExpression = null, + [CallerFilePath] string callerFilePath = "", + [CallerMemberName] string callerMemberName = "", + [CallerLineNumber] int callerLineNumber = 0 + ) => + LlmMarkdownAssertions.ConvertsToLlmWithMetadata( + expected, + await BuildAsync(), + new CallSite(callerFilePath, callerMemberName, callerLineNumber, expectedExpression) + ); // ──────────────────────────────────────────────────────────────────────────────── // JSON output @@ -222,8 +263,20 @@ public async Task ConvertsToLlmWithMetadata([LanguageInjection("markdown")] stri /// Asserts that the file at in the write file system /// contains JSON that normalizes to . /// - public async Task ConvertsToJson(string artifactPath, [LanguageInjection("json")] string expected) => - ResultsAssertions.ConvertsToJson(artifactPath, expected, await BuildAsync()); + public async Task ConvertsToJson( + string artifactPath, + [LanguageInjection("json")] string? expected = null, + [CallerArgumentExpression(nameof(expected))] string? expectedExpression = null, + [CallerFilePath] string callerFilePath = "", + [CallerMemberName] string callerMemberName = "", + [CallerLineNumber] int callerLineNumber = 0 + ) => + ResultsAssertions.ConvertsToJson( + artifactPath, + expected, + await BuildAsync(), + new CallSite(callerFilePath, callerMemberName, callerLineNumber, expectedExpression) + ); } /// @@ -368,7 +421,13 @@ public sealed class MarkdownResultTask(Task inner) /// Asserts the file's HTML matches the expected fragment exactly (pretty-printed diff). /// Mirrors F# toHtml. /// - public async Task ToHtml([LanguageInjection("html")] string expected) => HtmlAssertions.ToHtml(expected, await inner); + public async Task ToHtml( + [LanguageInjection("html")] string? expected = null, + [CallerArgumentExpression(nameof(expected))] string? expectedExpression = null, + [CallerFilePath] string callerFilePath = "", + [CallerMemberName] string callerMemberName = "", + [CallerLineNumber] int callerLineNumber = 0 + ) => HtmlAssertions.ToHtml(expected, await inner, new CallSite(callerFilePath, callerMemberName, callerLineNumber, expectedExpression)); /// Finds all instances in the fully-parsed document. public async Task Parses() where T : Markdig.Syntax.MarkdownObject => await MarkdownDocumentAssertions.Parses(await inner); diff --git a/tests/Elastic.Authoring.Tests/LlmMarkdown/LlmMarkdownOutput.cs b/tests/Elastic.Authoring.Tests/LlmMarkdown/LlmMarkdownOutput.cs index 2397a70bcc..327ac74f33 100644 --- a/tests/Elastic.Authoring.Tests/LlmMarkdown/LlmMarkdownOutput.cs +++ b/tests/Elastic.Authoring.Tests/LlmMarkdown/LlmMarkdownOutput.cs @@ -24,12 +24,11 @@ public class Headings : DocumentTest [Test, DisplayName("converts to standard markdown headings")] public async Task ConvertsToStandardMarkdownHeadings() => await Docs.ConvertsToNewLlm(""" - - ## Heading 2 + ## Heading 2 - ### Heading 3 - """); + ### Heading 3 + """); } public class CodeBlocks : DocumentTest @@ -46,13 +45,14 @@ def hello(): [Test, DisplayName("renders code blocks")] public async Task RendersCodeBlocks() => - await Docs.ConvertsToNewLlm(""" - - ```python - def hello(): - print("Hello, world!") - ``` - """); + await Docs.ConvertsToNewLlm( + """ + ```python + def hello(): + print("Hello, world!") + ``` + """ + ); } public class EnhancedCodeBlocks : DocumentTest @@ -69,13 +69,14 @@ 1. This is a callout [Test, DisplayName("converts to code block with optional caption comment")] public async Task ConvertsToCodeBlockWithOptionalCaptionComment() => - await Docs.ConvertsToNewLlm(""" - - ```python - def hello(): - print("Hello, world!") - ``` - """); + await Docs.ConvertsToNewLlm( + """ + ```python + def hello(): + print("Hello, world!") + ``` + """ + ); } public class Lists : DocumentTest @@ -99,27 +100,28 @@ 2. Nested ordered item 2 3. Ordered item 3 """; + // The expected output below is what the exporter writes today. Until it moved to snapshots this test compared with + // whitespace ignored, so it did not notice that the second level of nesting loses its indentation. Worth a look. [Test, DisplayName("converts to standard markdown lists")] public async Task ConvertsToStandardMarkdownLists() => await Docs.ConvertsToNewLlm( """ - - - Item 1 - - Item 2 - - Nested item 1 - - Nested item 2 - - Nested Nested item 1 - - Nested Nested item 2 - - Item 3 - - 1. Ordered item 1 - 2. Ordered item 2 - 1. Nested ordered item 1 - 2. Nested ordered item 2 - - Nested unordered item 1 - - Nested unordered item 2 - 3. Ordered item 3 - """ + - Item 1 + - Item 2 + - Nested item 1 + - Nested item 2 + - Nested Nested item 1 + - Nested Nested item 2 + - Item 3 + + 1. Ordered item 1 + 2. Ordered item 2 + 1. Nested ordered item 1 + 2. Nested ordered item 2 + - Nested unordered item 1 + - Nested unordered item 2 + 3. Ordered item 3 + """ ); } @@ -137,12 +139,11 @@ public class Tables : DocumentTest public async Task ConvertsToStandardMarkdownTables() => await Docs.ConvertsToNewLlm( """ - - | Header 1 | Header 2 | - |----------|----------| - | Cell 1 | Cell 2 | - | Cell 3 | Cell 4 | - """ + | Header 1 | Header 2 | + |----------|----------| + | Cell 1 | Cell 2 | + | Cell 3 | Cell 4 | + """ ); } @@ -353,17 +354,16 @@ public class AppliesSwitchDirective : DocumentTest public async Task RendersAppliesSwitchWithHumanReadableAppliesTo() => await Docs.ConvertsToNewLlm( """ - - - - Content for Elastic Stack users. - - - - Content for Serverless users. - - - """ + + + Content for Elastic Stack users. + + + + Content for Serverless users. + + + """ ); } @@ -396,29 +396,28 @@ It can *span* multiple lines and supports inline formatting. public async Task RendersCorrectly() => await Docs.ConvertsToNewLlm( """ - - - This is a note admonition. - - - - This is a warning admonition. - - - - This is a tip admonition. - - - - This is a tip admonition. - - - - It can *span* multiple lines and supports inline formatting.Here is a list: - - Item 1 - - Item 2 - - """ + + This is a note admonition. + + + + This is a warning admonition. + + + + This is a tip admonition. + + + + This is a tip admonition. + + + + It can *span* multiple lines and supports inline formatting.Here is a list: + - Item 1 + - Item 2 + + """ ); } @@ -452,27 +451,26 @@ This is a custom admonition with applies_to information. public async Task RendersCorrectlyWithAppliesToInformation() => await Docs.ConvertsToNewLlm( """ + + This is a note admonition with applies_to information. + - - This is a note admonition with applies_to information. - + + This is a warning admonition with applies_to information. + - - This is a warning admonition with applies_to information. - + + This is a tip admonition with applies_to information. + - - This is a tip admonition with applies_to information. - + + This is an important admonition with applies_to information. + - - This is an important admonition with applies_to information. - - - - This is a custom admonition with applies_to information. - - """ + + This is a custom admonition with applies_to information. + + """ ); } @@ -490,11 +488,10 @@ This note has multiple lifecycle states. public async Task RendersMultipleLifecyclesWithProductNameForEach() => await Docs.ConvertsToNewLlm( """ - - - This note has multiple lifecycle states. - - """ + + This note has multiple lifecycle states. + + """ ); } @@ -510,10 +507,7 @@ public class ImageDirective : DocumentTest [Test, DisplayName("converts to standard markdown image")] public async Task ConvertsToStandardMarkdownImage() => - await Docs.ConvertsToNewLlm(""" - - ![Alt text](https://www.elastic.co/path/to/image.png) - """); + await Docs.ConvertsToNewLlm("![Alt text](https://www.elastic.co/path/to/image.png)"); } public class IncludeDirective : GeneratorTest @@ -535,10 +529,9 @@ public class IncludeDirective : GeneratorTest [Test, DisplayName("handles include directives appropriately")] public async Task HandlesIncludeDirectivesAppropriately() => await Docs.ConvertsToNewLlm(""" - - - List item - - Nested list item - """); + - List item + - Nested list item + """); } public class MultipleElements : DocumentTest @@ -564,21 +557,20 @@ def hello(): public async Task ConvertsComplexDocumentToCleanMarkdown() => await Docs.ConvertsToNewLlm( """ + This is a paragraph with **bold** and *italic* text. + ```python + def hello(): + print("Hello, world!") + ``` - This is a paragraph with **bold** and *italic* text. - ```python - def hello(): - print("Hello, world!") - ``` - - - List item 1 - - List item 2 + - List item 1 + - List item 2 - | Header 1 | Header 2 | - |----------|----------| - | Cell 1 | Cell 2 | - """ + | Header 1 | Header 2 | + |----------|----------| + | Cell 1 | Cell 2 | + """ ); } @@ -603,18 +595,17 @@ def hello(): public async Task RenderedCorrectly() => await Docs.ConvertsToNewLlm( """ - - - List item 1 - ```python - def hello(): - print("Hello, world!") - ``` - - List item 2 - - - Nested list item 1 - - Nested list item 2 - - """ + - List item 1 + ```python + def hello(): + print("Hello, world!") + ``` + - List item 2 + + - Nested list item 1 + - Nested list item 2 + + """ ); } @@ -639,17 +630,16 @@ public class Tabs : DocumentTest public async Task RenderedCorrectly() => await Docs.ConvertsToNewLlm( """ - - - - This is where the content for tab #1 goes. - - - - This is where the content for tab #2 goes. - - - """ + + + This is where the content for tab #1 goes. + + + + This is where the content for tab #2 goes. + + + """ ); } @@ -685,11 +675,10 @@ public class Dropdown : DocumentTest public async Task RenderedCorrectly() => await Docs.ConvertsToNewLlm( """ - - - This is where the content for the dropdown goes. - - """ + + This is where the content for the dropdown goes. + + """ ); } @@ -707,11 +696,10 @@ public class DropdownWithAppliesTo : DocumentTest public async Task RenderedCorrectly() => await Docs.ConvertsToNewLlm( """ - - - This is where the content for the dropdown goes. - - """ + + This is where the content for the dropdown goes. + + """ ); } @@ -735,21 +723,20 @@ public class DefinitionList : DocumentTest public async Task RenderedCorrectly() => await Docs.ConvertsToNewLlm( """ - - - - This is the definition of the first term. - - This a list in a definition - - This a list in a definition - - - This is one definition of the second term. - ```javascript - console.log("Hello, world!"); - ``` - - - """ + + + This is the definition of the first term. + - This a list in a definition + - This a list in a definition + + + This is one definition of the second term. + ```javascript + console.log("Hello, world!"); + ``` + + + """ ); } @@ -763,10 +750,7 @@ public class ImageInline : DocumentTest [Test, DisplayName("rendered correctly")] public async Task RenderedCorrectly() => await Docs.ConvertsToNewLlm( - """ - - ![elasticsearch](https://www.elastic.co/images/64x64_Color_elasticsearch-logo-color-64px.png "elasticsearch") - """ + "![elasticsearch](https://www.elastic.co/images/64x64_Color_elasticsearch-logo-color-64px.png \"elasticsearch\")" ); } @@ -777,10 +761,7 @@ public class KbdRole : DocumentTest """; [Test, DisplayName("rendered correctly")] - public async Task RenderedCorrectly() => await Docs.ConvertsToNewLlm(""" - - Cmd + Enter - """); + public async Task RenderedCorrectly() => await Docs.ConvertsToNewLlm("Cmd + Enter"); } public class CodeblockInList : DocumentTest @@ -800,23 +781,24 @@ def hello(): ``` """; + // The expected output below is what the exporter writes today. Until it moved to snapshots this test compared with + // whitespace ignored, so it did not notice that the fenced code block in a nested list item loses its indentation. Worth a look. [Test, DisplayName("rendered correctly")] public async Task RenderedCorrectly() => await Docs.ConvertsToNewLlm( """ - - - List item 1 - ```python - def hello(): - print("Hello, world!") - ``` - - List item 2 - 1. Nested list item - ```python - def hello(): - print("Hello, world!") - ``` - """ + - List item 1 + ```python + def hello(): + print("Hello, world!") + ``` + - List item 2 + 1. Nested list item + ```python + def hello(): + print("Hello, world!") + ``` + """ ); } @@ -837,10 +819,9 @@ public class Substitutions : DocumentTest public async Task RenderedCorrectly() => await Docs.ConvertsToNewLlm( """ - - Hello, this is a substitution: Hello World! - This is not a substitution: {{not-found}} - """ + Hello, this is a substitution: Hello World! + This is not a substitution: {{not-found}} + """ ); } @@ -866,15 +847,14 @@ public class SubstitutionInCodeblock : DocumentTest public async Task SubstitutionInCodeblockIsOnlyReplacedWhenSubsIsTrue() => await Docs.ConvertsToNewLlm( """ + ```plaintext + Hello, this is a substitution: {{hello-world}} + ``` - ```plaintext - Hello, this is a substitution: {{hello-world}} - ``` - - ```plaintext - Hello, this is a substitution: Hello World! - ``` - """ + ```plaintext + Hello, this is a substitution: Hello World! + ``` + """ ); } @@ -896,16 +876,15 @@ flowchart LR public async Task RendersMermaidAsCodeBlock() => await Docs.ConvertsToNewLlm( """ - - ```mermaid - flowchart LR - A[Start] --> B{Decision} - B -->|Yes| C[Action 1] - B -->|No| D[Action 2] - C --> E[End] - D --> E - ``` - """ + ```mermaid + flowchart LR + A[Start] --> B{Decision} + B -->|Yes| C[Action 1] + B -->|No| D[Action 2] + C --> E[End] + D --> E + ``` + """ ); } @@ -922,10 +901,7 @@ public class SubstitutionInHeading : DocumentTest """; [Test, DisplayName("renders correctly")] - public async Task RendersCorrectly() => await Docs.ConvertsToNewLlm(""" - - ## Hello, World! - """); + public async Task RendersCorrectly() => await Docs.ConvertsToNewLlm("## Hello, World!"); } public class SettingsDirective : GeneratorTest @@ -971,41 +947,40 @@ public class SettingsDirective : GeneratorTest public async Task RendersSettingsAsMarkdownHeadings() => await Docs.ConvertsToNewLlm( """ - - ## General settings - - - Elastic Stack: Planned - Self-managed Elastic deployments: Planned - - This is a test setting with **bold** text and a [link](https://example.com). - Datatype: `enum` - Default: `strict` - Options: - - `strict` - - `lenient` - - - Elastic Stack: Planned - Self-managed Elastic deployments: Planned - - Child setting description. - Datatype: `string` - - - - Another setting description. - - - - ## Advanced settings - - - - An advanced option. - - - """ + ## General settings + + + Elastic Stack: Planned + Self-managed Elastic deployments: Planned + + This is a test setting with **bold** text and a [link](https://example.com). + Datatype: `enum` + Default: `strict` + Options: + - `strict` + - `lenient` + + + Elastic Stack: Planned + Self-managed Elastic deployments: Planned + + Child setting description. + Datatype: `string` + + + + Another setting description. + + + + ## Advanced settings + + + + An advanced option. + + + """ ); [Test, DisplayName("renders group headings one level deeper than preceding markdown heading")] @@ -1038,26 +1013,25 @@ public async Task RendersGroupHeadingsOneLevelDeeperThanPrecedingMarkdownHeading ]); await scenario.ConvertsToNewLlm( """ + ## Included settings file - ## Included settings file + ### General settings + + - ### General settings - - + Test. + + - Test. - - + ### Advanced settings + + - ### Advanced settings - - - - Advanced. - - - """ + Advanced. + + + """ ); } } @@ -1090,12 +1064,11 @@ public class LinksInTables : DocumentTest public async Task RendersLinksInTableCellsWithoutDuplication() => await Docs.ConvertsToNewLlm( """ - - | Feature | Availability | - |--------------------------------------------------------------------------------|--------------| - | [Security configurations](https://www.elastic.co/docs/deploy-manage/security) | Full control | - | [Authentication realms](https://www.elastic.co/docs/deploy-manage/users-roles) | Available | - """ + | Feature | Availability | + |--------------------------------------------------------------------------------|--------------| + | [Security configurations](https://www.elastic.co/docs/deploy-manage/security) | Full control | + | [Authentication realms](https://www.elastic.co/docs/deploy-manage/users-roles) | Available | + """ ); } @@ -1112,11 +1085,10 @@ public class MultipleLinksInTableCells : DocumentTest public async Task RendersMultipleLinksInSameCellWithoutDuplication() => await Docs.ConvertsToNewLlm( """ - - | Feature | Links | - |----------|---------------------------------------------------------------------------| - | Security | [Config](https://example.com/config) and [Auth](https://example.com/auth) | - """ + | Feature | Links | + |----------|---------------------------------------------------------------------------| + | Security | [Config](https://example.com/config) and [Auth](https://example.com/auth) | + """ ); } @@ -1134,12 +1106,11 @@ public class LinksWithFormattingInTables : DocumentTest public async Task RendersFormattedLinksInTableCellsCorrectly() => await Docs.ConvertsToNewLlm( """ - - | Feature | Description | - |---------------------------------------------|-------------| - | [**Bold link**](https://example.com) | Description | - | [*Italic link*](https://example.com/italic) | Another | - """ + | Feature | Description | + |---------------------------------------------|-------------| + | [**Bold link**](https://example.com) | Description | + | [*Italic link*](https://example.com/italic) | Another | + """ ); } @@ -1158,13 +1129,12 @@ public class BoldAndItalicInTables : DocumentTest public async Task RendersBoldAndItalicInTableCellsWithoutDuplication() => await Docs.ConvertsToNewLlm( """ - - | Format | Example | - |--------|-------------------------------| - | Bold | This is **bold text** here | - | Italic | This is *italic text* here | - | Both | This is **bold** and *italic* | - """ + | Format | Example | + |--------|-------------------------------| + | Bold | This is **bold text** here | + | Italic | This is *italic text* here | + | Both | This is **bold** and *italic* | + """ ); } @@ -1182,12 +1152,11 @@ public class CodeInlineInTables : DocumentTest public async Task RendersCodeInlineInTableCellsCorrectly() => await Docs.ConvertsToNewLlm( """ - - | Command | Description | - |--------------|-----------------| - | `git status` | Shows status | - | `git commit` | Commits changes | - """ + | Command | Description | + |--------------|-----------------| + | `git status` | Shows status | + | `git commit` | Commits changes | + """ ); } @@ -1204,11 +1173,10 @@ public class ImagesInTables : DocumentTest public async Task RendersImagesInTableCellsWithoutDuplication() => await Docs.ConvertsToNewLlm( """ - - | Icon | Name | - |---------------------------------------|------| - | ![logo](https://example.com/logo.png) | Logo | - """ + | Icon | Name | + |---------------------------------------|------| + | ![logo](https://example.com/logo.png) | Logo | + """ ); } @@ -1236,13 +1204,12 @@ public class CsvIncludeDirective : GeneratorTest public async Task RendersCsvAsMarkdownTable() => await Docs.ConvertsToNewLlm( """ - - | Name | Age | City | - |-------------|-----|-------------| - | John Doe | 30 | New York | - | Jane Smith | 25 | Los Angeles | - | Bob Johnson | 35 | Chicago | - """ + | Name | Age | City | + |-------------|-----|-------------| + | John Doe | 30 | New York | + | Jane Smith | 25 | Los Angeles | + | Bob Johnson | 35 | Chicago | + """ ); } @@ -1270,14 +1237,13 @@ public class CsvIncludeDirectiveWithCaption : GeneratorTest public async Task RendersCsvWithCaptionAsBoldTitle() => await Docs.ConvertsToNewLlm( """ + **Product List** - **Product List** - - | Product | Price | Stock | - |---------|-------|-------| - | Widget | 9.99 | 100 | - | Gadget | 19.99 | 50 | - """ + | Product | Price | Stock | + |---------|-------|-------| + | Widget | 9.99 | 100 | + | Gadget | 19.99 | 50 | + """ ); } @@ -1302,11 +1268,10 @@ public class CsvIncludeDirectiveWithCustomSeparator : GeneratorTest public async Task ParsesCsvWithCustomSeparator() => await Docs.ConvertsToNewLlm( """ - - | Name | Value | - |-------|-------| - | Item1 | 100 | - | Item2 | 200 | - """ + | Name | Value | + |-------|-------| + | Item1 | 100 | + | Item2 | 200 | + """ ); } diff --git a/tests/Elastic.Authoring.Tests/PlainText/PlainTextOutput.cs b/tests/Elastic.Authoring.Tests/PlainText/PlainTextOutput.cs index f37712ffe7..e506d7deae 100644 --- a/tests/Elastic.Authoring.Tests/PlainText/PlainTextOutput.cs +++ b/tests/Elastic.Authoring.Tests/PlainText/PlainTextOutput.cs @@ -296,7 +296,10 @@ public class MermaidCodeBlock : DocumentTest [Test, DisplayName("renders mermaid as code content")] public async Task RendersMermaidAsCodeContent() => // Mermaid code blocks render like any code block - await Docs.ConvertsToPlainText("flowchart LR\nA --> B"); + await Docs.ConvertsToPlainText(""" + flowchart LR + A --> B + """); } public class CsvIncludeDirective : GeneratorTest From 63c3904df68b88221a413c6771ca7a5730142c62 Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Thu, 5 Nov 2026 12:45:20 +0100 Subject: [PATCH 4/8] Authoring tests: whole-output snapshots instead of fragment assertions Where a test class asserted a cluster of ConvertsToContainingHtml, ConvertsToContainingRawHtml and DoesNotConvertToContainingHtml fragments on one page, the cluster is now one snapshot of the page content: inline (ConvertsToHtml) up to about 45 lines, a verified .html file (ConvertsToHtmlSnapshot) beyond that, 7 in all. Classes asserting HasError, HasWarning or HasHint now snapshot every diagnostic as 'severity file:line message' (HasDiagnostics), which also absorbs their HasNoErrors and HasNoWarnings. Other HasNoErrors/HasNoWarnings stay. These tests became stricter: they used to look at the first diagnostic's substring or at fragments, and now cover the whole output. Before accepting, every old fragment was checked to appear in the snapshot (whitespace and entities normalised) and every negative fragment to be absent, and every old diagnostic substring to appear in the diagnostic text; the only mismatches were valueless attributes that AngleSharp prints as name="" (alt, open, checked, hidden, data-*). Kept as targeted assertions, because a section snapshot cannot show them: - the newline between elements inside
     (ConsoleCodeBlockSegments);
    -  meta tags (ProductsFrontMatter);
    - the page shell, header, footer and layouts (DesignSystem), whose whole
      page carries the sprite sheet and the checkout directory name.
    
    Dropped the helpers nothing uses any more (ConvertsToContainingHtml,
    ContainsHtml, DoesNotContainHtml on the result task, the DiffPlex line diff)
    and the DiffPlex package reference.
    
    Co-Authored-By: Claude Sonnet 5.5 
    ---
     .../RendersHtml.verified.html                 |  47 ++
     .../RendersHtml.verified.html                 |  49 +++
     .../RendersHtml.verified.html                 |  58 +++
     .../RendersHtml.verified.html                 | 116 +++++
     .../RendersHtml.verified.html                 |  46 ++
     .../RendersHtml.verified.html                 |  65 +++
     .../RendersHtml.verified.html                 | 141 ++++++
     .../Applicability/AppliesToDirective.cs       |  15 +-
     .../Applicability/AppliesToFrontMatter.cs     |  68 ++-
     .../Blocks/AgentSkill.cs                      |  10 +-
     .../Blocks/CodeBlocks/CodeBlocks.cs           |   5 +-
     .../Blocks/Hub/CardsAndExplore.cs             | 367 ++++++++++------
     .../Blocks/Hub/GetStarted.cs                  | 284 +++++++++---
     .../Blocks/Hub/Hero.cs                        | 119 +++--
     .../Blocks/Hub/OnThisPage.cs                  |  21 +-
     .../Blocks/Hub/WhatsNew.cs                    | 232 ++++++++--
     .../Blocks/ImageBlocks.cs                     | 162 +++++--
     .../Blocks/NestedDirectiveOptions.cs          | 120 +++--
     .../Blocks/PageCards.cs                       |  18 +-
     .../Blocks/Storybook.cs                       |  10 +-
     .../DesignSystem/DesignSystemDirectives.cs    | 412 +++++++++++-------
     .../Elastic.Authoring.Tests.csproj            |   1 -
     .../Framework/ErrorCollectorAssertions.cs     |   5 +
     .../Framework/HtmlAssertions.cs               |  57 +--
     .../Framework/Setup.cs                        |  41 +-
     .../Inline/CrossLinks.cs                      |  52 ++-
     .../Inline/InlineAnchors.cs                   |  70 ++-
     .../Inline/InlineLinks.cs                     |  17 +-
     .../Inline/Substitutions.cs                   |   5 +-
     .../Linters/SpaceNormalizers.cs               |   8 +-
     30 files changed, 1971 insertions(+), 650 deletions(-)
     create mode 100644 tests/Elastic.Authoring.Tests/.snapshots/Blocks/Hub/CardGroupNestedInExplore/RendersHtml.verified.html
     create mode 100644 tests/Elastic.Authoring.Tests/.snapshots/Blocks/Hub/GetStartedWithFiveStepsAfterTheBox/RendersHtml.verified.html
     create mode 100644 tests/Elastic.Authoring.Tests/.snapshots/Blocks/Hub/GetStartedWithFourSteps/RendersHtml.verified.html
     create mode 100644 tests/Elastic.Authoring.Tests/.snapshots/Blocks/Hub/OnThisPageWithAllSectionKinds/RendersHtml.verified.html
     create mode 100644 tests/Elastic.Authoring.Tests/.snapshots/Blocks/Hub/WhatsNewWithAnInlineBody/RendersHtml.verified.html
     create mode 100644 tests/Elastic.Authoring.Tests/.snapshots/DesignSystem/AppliesToComponent/RendersHtml.verified.html
     create mode 100644 tests/Elastic.Authoring.Tests/.snapshots/DesignSystem/VectorSizingComponent/RendersHtml.verified.html
    
    diff --git a/tests/Elastic.Authoring.Tests/.snapshots/Blocks/Hub/CardGroupNestedInExplore/RendersHtml.verified.html b/tests/Elastic.Authoring.Tests/.snapshots/Blocks/Hub/CardGroupNestedInExplore/RendersHtml.verified.html
    new file mode 100644
    index 0000000000..7ac0e744b3
    --- /dev/null
    +++ b/tests/Elastic.Authoring.Tests/.snapshots/Blocks/Hub/CardGroupNestedInExplore/RendersHtml.verified.html
    @@ -0,0 +1,47 @@
    +
    +

    Explore the docs

    +

    Organized by task.

    +
    +
    + +
    + +

    Authoring

    + +
    +
    +
      +
    • +

      + Syntax +

      +
    • +
    +
    +
    +
    diff --git a/tests/Elastic.Authoring.Tests/.snapshots/Blocks/Hub/GetStartedWithFiveStepsAfterTheBox/RendersHtml.verified.html b/tests/Elastic.Authoring.Tests/.snapshots/Blocks/Hub/GetStartedWithFiveStepsAfterTheBox/RendersHtml.verified.html new file mode 100644 index 0000000000..7d1ba07caa --- /dev/null +++ b/tests/Elastic.Authoring.Tests/.snapshots/Blocks/Hub/GetStartedWithFiveStepsAfterTheBox/RendersHtml.verified.html @@ -0,0 +1,49 @@ +
    +
    +

    Get started

    +
    +
    +
      +
    1. +
      + +

      One

      +
      +
    2. +
    +
      +
    1. +
      + +

      Two

      +
      +
      +
      +
    2. +
    3. +
      + +

      Three

      +
      +
      +
      +
    4. +
    5. +
      + +

      Four

      +
      +
      +
      +
    6. +
    7. +
      + +

      Five

      +
      +
      +
      +
    8. +
    +
    +
    diff --git a/tests/Elastic.Authoring.Tests/.snapshots/Blocks/Hub/GetStartedWithFourSteps/RendersHtml.verified.html b/tests/Elastic.Authoring.Tests/.snapshots/Blocks/Hub/GetStartedWithFourSteps/RendersHtml.verified.html new file mode 100644 index 0000000000..85274d9694 --- /dev/null +++ b/tests/Elastic.Authoring.Tests/.snapshots/Blocks/Hub/GetStartedWithFourSteps/RendersHtml.verified.html @@ -0,0 +1,58 @@ +
    +
    +

    Get started

    +
    +
    +
      +
    1. +
      + +

      Install

      +
      +
      +
      + Source +
      +
      +								dotnet build
      +							
      +
      +
      + +
      + Container + Get started +
      +
      +
    2. +
    +
      +
    1. +
      + +

      Write

      +
      +
      +
      +
    2. +
    3. +
      + +

      Preview

      +
      +
      +
      +
    4. +
    5. +
      + +

      Validate

      +
      +
      +
      +
    6. +
    +
    +
    diff --git a/tests/Elastic.Authoring.Tests/.snapshots/Blocks/Hub/OnThisPageWithAllSectionKinds/RendersHtml.verified.html b/tests/Elastic.Authoring.Tests/.snapshots/Blocks/Hub/OnThisPageWithAllSectionKinds/RendersHtml.verified.html new file mode 100644 index 0000000000..2f8ae3ba33 --- /dev/null +++ b/tests/Elastic.Authoring.Tests/.snapshots/Blocks/Hub/OnThisPageWithAllSectionKinds/RendersHtml.verified.html @@ -0,0 +1,116 @@ + +
    +
    +

    Get started with Kibana

    +
    +
    +
      +
    1. +
      + +

      Install

      +
      +

      Install it.

      +
    2. +
    +
    +
    +
    +
    +
    +

    What's new

    +
    +
    + +
    +
    +

    Solutions

    +
    +
      +
    • +
      +

      + Search +

      +
      +
    • +
    +
    +

    Explore Kibana docs

    +
    +
    + +
    +
    +

    Use Kibana

    +
    +
    +
    + +

    Analyze data

    + +
    +
    +
      +
    • +
      + Discover +
      +
    • +
    +
    +
    +
    diff --git a/tests/Elastic.Authoring.Tests/.snapshots/Blocks/Hub/WhatsNewWithAnInlineBody/RendersHtml.verified.html b/tests/Elastic.Authoring.Tests/.snapshots/Blocks/Hub/WhatsNewWithAnInlineBody/RendersHtml.verified.html new file mode 100644 index 0000000000..761e2711f2 --- /dev/null +++ b/tests/Elastic.Authoring.Tests/.snapshots/Blocks/Hub/WhatsNewWithAnInlineBody/RendersHtml.verified.html @@ -0,0 +1,46 @@ +
    +
    +
    +

    What's new in docs-builder

    +

    Recent additions to the toolchain.

    +
    +
    + +
    diff --git a/tests/Elastic.Authoring.Tests/.snapshots/DesignSystem/AppliesToComponent/RendersHtml.verified.html b/tests/Elastic.Authoring.Tests/.snapshots/DesignSystem/AppliesToComponent/RendersHtml.verified.html new file mode 100644 index 0000000000..5879760ff9 --- /dev/null +++ b/tests/Elastic.Authoring.Tests/.snapshots/DesignSystem/AppliesToComponent/RendersHtml.verified.html @@ -0,0 +1,65 @@ +
    +
    + + + + + + Note + + + + + The + Elastic Stack + includes Elastic's core products such as Elasticsearch, Kibana, Logstash, and Beats. + Availability + + + Planned + We plan to add this functionality in a future Elastic Stack update. Subject to changes. + + + Unless stated otherwise on the page, this functionality is available when your Elastic Stack is deployed on Elastic Cloud Hosted, Elastic Cloud Enterprise, Elastic Cloud on Kubernetes, and self-managed environments. + This documentation corresponds to the latest patch available for each minor version. If you're not using the latest patch, check the + release notes + for changes. + + + +
    +
    +

    This applies to the stack.

    +
    +
    +

    Inline badge: + + + + + The + Elastic Stack + includes Elastic's core products such as Elasticsearch, Kibana, Logstash, and Beats. + Availability + + + Planned + We plan to add this functionality in a future Elastic Stack update. Subject to changes. + + + Unless stated otherwise on the page, this functionality is available when your Elastic Stack is deployed on Elastic Cloud Hosted, Elastic Cloud Enterprise, Elastic Cloud on Kubernetes, and self-managed environments. + This documentation corresponds to the latest patch available for each minor version. If you're not using the latest patch, check the + release notes + for changes. + + + +

    diff --git a/tests/Elastic.Authoring.Tests/.snapshots/DesignSystem/VectorSizingComponent/RendersHtml.verified.html b/tests/Elastic.Authoring.Tests/.snapshots/DesignSystem/VectorSizingComponent/RendersHtml.verified.html new file mode 100644 index 0000000000..896c599718 --- /dev/null +++ b/tests/Elastic.Authoring.Tests/.snapshots/DesignSystem/VectorSizingComponent/RendersHtml.verified.html @@ -0,0 +1,141 @@ + diff --git a/tests/Elastic.Authoring.Tests/Applicability/AppliesToDirective.cs b/tests/Elastic.Authoring.Tests/Applicability/AppliesToDirective.cs index 920deba723..9ac2c6d69d 100644 --- a/tests/Elastic.Authoring.Tests/Applicability/AppliesToDirective.cs +++ b/tests/Elastic.Authoring.Tests/Applicability/AppliesToDirective.cs @@ -79,11 +79,9 @@ public class WarnsOnOldSyntax : MarkdownTest ``` """; - [Test, DisplayName("has no errors")] - public async Task HasNoErrors() => await Docs.HasNoErrors(); - - [Test, DisplayName("warns on bad syntax")] - public async Task WarnsOnBadSyntax() => await Docs.HasWarning("Applies block does not use valid yaml keys: :hosted"); + [Test, DisplayName("reports diagnostics")] + public async Task ReportsDiagnostics() => + await Docs.HasDiagnostics("warning {repo}/docs/index.md:2 Applies block does not use valid yaml keys: :hosted"); } public class HintsOnInvalidKeys : MarkdownTest @@ -94,6 +92,9 @@ public class HintsOnInvalidKeys : MarkdownTest ``` """; - [Test, DisplayName("hints on unknown key")] - public async Task HintsOnUnknownKey() => await Docs.HasHint("Unknown key 'hosted' in applies_to."); + [Test, DisplayName("reports diagnostics")] + public async Task ReportsDiagnostics() => + await Docs.HasDiagnostics( + "hint {repo}/docs/index.md:2 Unknown key 'hosted' in applies_to. Valid keys: apm_agent_dotnet, deployment, ecctl, ece, ech, eck, elasticsearch, elasticsearch, ess, observability, product, security, …." + ); } diff --git a/tests/Elastic.Authoring.Tests/Applicability/AppliesToFrontMatter.cs b/tests/Elastic.Authoring.Tests/Applicability/AppliesToFrontMatter.cs index cf56c75527..805f9e8947 100644 --- a/tests/Elastic.Authoring.Tests/Applicability/AppliesToFrontMatter.cs +++ b/tests/Elastic.Authoring.Tests/Applicability/AppliesToFrontMatter.cs @@ -304,8 +304,11 @@ public class BothEssAndEchDefinedUsesEchValueAndWarns : AuthoringTest public async Task EchValueWins() => await Docs.AppliesTo(new ApplicableTo { Deployment = new DeploymentApplicability { Ess = Applies("beta") } }); - [Test, DisplayName("emits warning about both being defined")] - public async Task EmitsWarning() => await Docs.HasWarning("Both 'ess' and 'ech' are defined"); + [Test, DisplayName("reports diagnostics")] + public async Task ReportsDiagnostics() => + await Docs.HasDiagnostics( + "warning {repo}/docs/index.md Both 'ess' and 'ech' are defined. Move 'ess' content into 'ech' to avoid information loss." + ); } public class ParsesEchAtTopLevel : AuthoringTest @@ -333,8 +336,9 @@ public class ParsesProductComingDeprecated : AuthoringTest product: coming 9.5 """); - [Test, DisplayName("should warn of deprecated lifecycle state")] - public async Task WarnOfDeprecatedLifecycle() => await Docs.HasHint("The 'coming' lifecycle is deprecated and will be removed"); + [Test, DisplayName("reports diagnostics")] + public async Task ReportsDiagnostics() => + await Docs.HasDiagnostics("hint {repo}/docs/index.md The 'coming' lifecycle is deprecated and will be removed in a future release."); } public class ParsesProductPlanned : AuthoringTest @@ -345,8 +349,11 @@ public class ParsesProductPlanned : AuthoringTest product: planned 9.5 """); - [Test, DisplayName("should warn of deprecated lifecycle state")] - public async Task WarnOfDeprecatedLifecycle() => await Docs.HasHint("The 'planned' lifecycle is deprecated and will be removed"); + [Test, DisplayName("reports diagnostics")] + public async Task ReportsDiagnostics() => + await Docs.HasDiagnostics( + "hint {repo}/docs/index.md The 'planned' lifecycle is deprecated and will be removed in a future release." + ); } public class ParsesProductRemoved : AuthoringTest @@ -600,8 +607,11 @@ public class ServerlessStringWithVersionEmitsError : AuthoringTest serverless: ga 9.5 """); - [Test, DisplayName("emits error for versioned serverless")] - public async Task EmitsErrorForVersionedServerless() => await Docs.HasError("Can't specify a version for 'serverless'"); + [Test, DisplayName("reports diagnostics")] + public async Task ReportsDiagnostics() => + await Docs.HasDiagnostics( + "error {repo}/docs/index.md Can't specify a version for 'serverless' because this product is not versioned. Remove the version, or use 'stack:' for version-specific requirements." + ); } public class ServerlessProjectWithVersionEmitsError : AuthoringTest @@ -613,8 +623,11 @@ public class ServerlessProjectWithVersionEmitsError : AuthoringTest elasticsearch: ga 9.5 """); - [Test, DisplayName("emits error for versioned elasticsearch project")] - public async Task EmitsError() => await Docs.HasError("Can't specify a version for 'elasticsearch'"); + [Test, DisplayName("reports diagnostics")] + public async Task ReportsDiagnostics() => + await Docs.HasDiagnostics( + "error {repo}/docs/index.md Can't specify a version for 'elasticsearch' because this product is not versioned. Remove the version, or use 'stack:' for version-specific requirements." + ); } public class ServerlessVectorDatabaseWithVersionEmitsError : AuthoringTest @@ -626,8 +639,11 @@ public class ServerlessVectorDatabaseWithVersionEmitsError : AuthoringTest vectordb: ga 9.5 """); - [Test, DisplayName("emits error for versioned vector database project")] - public async Task EmitsError() => await Docs.HasError("Can't specify a version for 'vectordb'"); + [Test, DisplayName("reports diagnostics")] + public async Task ReportsDiagnostics() => + await Docs.HasDiagnostics( + "error {repo}/docs/index.md Can't specify a version for 'vectordb' because this product is not versioned. Remove the version, or use 'stack:' for version-specific requirements." + ); } public class DeploymentEssWithVersionEmitsError : AuthoringTest @@ -639,8 +655,11 @@ public class DeploymentEssWithVersionEmitsError : AuthoringTest ess: ga 9.5 """); - [Test, DisplayName("emits error for versioned ess")] - public async Task EmitsError() => await Docs.HasError("Can't specify a version for 'ess'"); + [Test, DisplayName("reports diagnostics")] + public async Task ReportsDiagnostics() => + await Docs.HasDiagnostics( + "error {repo}/docs/index.md Can't specify a version for 'ess' because this product is not versioned. Remove the version, or use 'stack:' for version-specific requirements." + ); } public class DeploymentEchWithVersionEmitsError : AuthoringTest @@ -652,8 +671,11 @@ public class DeploymentEchWithVersionEmitsError : AuthoringTest ech: preview 9.5 """); - [Test, DisplayName("emits error for versioned ech")] - public async Task EmitsError() => await Docs.HasError("Can't specify a version for 'ech'"); + [Test, DisplayName("reports diagnostics")] + public async Task ReportsDiagnostics() => + await Docs.HasDiagnostics( + "error {repo}/docs/index.md Can't specify a version for 'ech' because this product is not versioned. Remove the version, or use 'stack:' for version-specific requirements." + ); } public class TopLevelEchWithVersionEmitsError : AuthoringTest @@ -665,8 +687,11 @@ public class TopLevelEchWithVersionEmitsError : AuthoringTest stack: ga 9.1 """); - [Test, DisplayName("emits error for versioned top level ech")] - public async Task EmitsError() => await Docs.HasError("Can't specify a version for 'ech'"); + [Test, DisplayName("reports diagnostics")] + public async Task ReportsDiagnostics() => + await Docs.HasDiagnostics( + "error {repo}/docs/index.md Can't specify a version for 'ech' because this product is not versioned. Remove the version, or use 'stack:' for version-specific requirements." + ); } public class DeploymentStringShorthandWithVersionEmitsErrorForEss : AuthoringTest @@ -677,6 +702,9 @@ public class DeploymentStringShorthandWithVersionEmitsErrorForEss : AuthoringTes deployment: ga 9.0.0 """); - [Test, DisplayName("emits error because ess does not support versions")] - public async Task EmitsError() => await Docs.HasError("Can't specify a version for 'ess'"); + [Test, DisplayName("reports diagnostics")] + public async Task ReportsDiagnostics() => + await Docs.HasDiagnostics( + "error {repo}/docs/index.md Can't specify a version for 'ess' because this product is not versioned. Remove the version, or use 'stack:' for version-specific requirements." + ); } diff --git a/tests/Elastic.Authoring.Tests/Blocks/AgentSkill.cs b/tests/Elastic.Authoring.Tests/Blocks/AgentSkill.cs index c14885aafc..faafd1b9f2 100644 --- a/tests/Elastic.Authoring.Tests/Blocks/AgentSkill.cs +++ b/tests/Elastic.Authoring.Tests/Blocks/AgentSkill.cs @@ -94,8 +94,9 @@ public class AgentSkillMissingUrl : MarkdownTest ::: """; - [Test, DisplayName("has error")] - public async Task HasError() => await Docs.HasError("requires a :url: property"); + [Test, DisplayName("reports diagnostics")] + public async Task ReportsDiagnostics() => + await Docs.HasDiagnostics("error {repo}/docs/index.md:2 agent-skill directive requires a :url: property"); } public class AgentSkillRelativeUrl : MarkdownTest @@ -106,6 +107,7 @@ public class AgentSkillRelativeUrl : MarkdownTest ::: """; - [Test, DisplayName("has error")] - public async Task HasError() => await Docs.HasError("must be an absolute URL"); + [Test, DisplayName("reports diagnostics")] + public async Task ReportsDiagnostics() => + await Docs.HasDiagnostics("error {repo}/docs/index.md:2 agent-skill :url: must be an absolute URL, got '/relative/path'"); } diff --git a/tests/Elastic.Authoring.Tests/Blocks/CodeBlocks/CodeBlocks.cs b/tests/Elastic.Authoring.Tests/Blocks/CodeBlocks/CodeBlocks.cs index a2eddd09c3..8be573925b 100644 --- a/tests/Elastic.Authoring.Tests/Blocks/CodeBlocks/CodeBlocks.cs +++ b/tests/Elastic.Authoring.Tests/Blocks/CodeBlocks/CodeBlocks.cs @@ -14,8 +14,9 @@ public class WarnsOnInvalidLanguage : MarkdownTest ``` """; - [Test, DisplayName("validate HTML: generates link and alt attr")] - public async Task ValidateHtml() => await Docs.HasWarning("Unknown language: not-a-valid-language"); + [Test, DisplayName("reports diagnostics")] + public async Task ReportsDiagnostics() => + await Docs.HasDiagnostics("warning {repo}/docs/index.md:2 Unknown language: not-a-valid-language"); [Test, DisplayName("parses to EnhancedCodeBlock")] public async Task ParsesCodeBlock() diff --git a/tests/Elastic.Authoring.Tests/Blocks/Hub/CardsAndExplore.cs b/tests/Elastic.Authoring.Tests/Blocks/Hub/CardsAndExplore.cs index 2715b077e9..cb5eed257c 100644 --- a/tests/Elastic.Authoring.Tests/Blocks/Hub/CardsAndExplore.cs +++ b/tests/Elastic.Authoring.Tests/Blocks/Hub/CardsAndExplore.cs @@ -28,21 +28,32 @@ public class CardGroupStandalone : MarkdownTest :::: """; - [Test, DisplayName("renders a heading and a grid, not an accordion")] - public async Task RendersHeadingAndGrid() => - await Docs.ConvertsToContainingHtml( + [Test, DisplayName("renders HTML")] + public async Task RendersHtml() => + await Docs.ConvertsToHtml( """

    Get hands-on

    Follow a guided quickstart.

    + """ ); - [Test, DisplayName("renders the card with its description")] - public async Task RendersCardDescription() => - await Docs.ConvertsToContainingHtml("""

    Author a page and preview it.

    """); - [Test, DisplayName("has no errors")] public async Task HasNoErrors() => await Docs.HasNoErrors(); } @@ -62,8 +73,28 @@ public class CardGroupWithLinksInTheIntro : MarkdownTest :::: """; - [Test, DisplayName("renders inline links in the intro")] - public async Task RendersInlineLinksInTheIntro() => await Docs.ConvertsToContainingRawHtml("""solutions"""); + [Test, DisplayName("renders HTML")] + public async Task RendersHtml() => + await Docs.ConvertsToHtml( + """ +
    +

    Components

    +

    They power + solutions + and have several ways to + deploy.

    +
    +
      +
    • +
      +

      + Elasticsearch +

      +
      +
    • +
    + """ + ); [Test, DisplayName("has no errors")] public async Task HasNoErrors() => await Docs.HasNoErrors(); @@ -84,8 +115,26 @@ public class CardGroupWithAMarkdownLinkInTheIntro : MarkdownTest :::: """; - [Test, DisplayName("turns the markdown path into a page link")] - public async Task TurnsMarkdownPathIntoPageLink() => await Docs.ConvertsToContainingRawHtml("""Compare options"""); + [Test, DisplayName("renders HTML")] + public async Task RendersHtml() => + await Docs.ConvertsToHtml( + """ +
    +

    Deploy

    +

    Choose where to run it. + Compare options.

    +
    +
      +
    • +
      +

      + Serverless +

      +
      +
    • +
    + """ + ); [Test, DisplayName("has no errors")] public async Task HasNoErrors() => await Docs.HasNoErrors(); @@ -105,9 +154,26 @@ public class CardGroupWithATitledLinkInTheIntro : MarkdownTest :::: """; - [Test, DisplayName("resolves the link and keeps its title")] - public async Task ResolvesLinkAndKeepsTitle() => - await Docs.ConvertsToContainingRawHtml("""Compare options"""); + [Test, DisplayName("renders HTML")] + public async Task RendersHtml() => + await Docs.ConvertsToHtml( + """ +
    +

    Deploy

    +

    Choose where to run it. + Compare options.

    +
    +
      +
    • +
      +

      + Serverless +

      +
      +
    • +
    + """ + ); [Test, DisplayName("has no errors")] public async Task HasNoErrors() => await Docs.HasNoErrors(); @@ -127,8 +193,27 @@ public class CardGroupWithLinkSyntaxInCodeInTheIntro : MarkdownTest :::: """; - [Test, DisplayName("keeps the code span as written")] - public async Task KeepsCodeSpan() => await Docs.ConvertsToContainingRawHtml("[guide](/does-not-exist.md)"); + [Test, DisplayName("renders HTML")] + public async Task RendersHtml() => + await Docs.ConvertsToHtml( + """ +
    +

    Deploy

    +

    Write links as + [guide](/does-not-exist.md) + in your text.

    +
    +
      +
    • +
      +

      + Serverless +

      +
      +
    • +
    + """ + ); [Test, DisplayName("does not report a link inside code")] public async Task HasNoErrors() => await Docs.HasNoErrors(); @@ -148,8 +233,11 @@ public class CardGroupWithABrokenLinkInTheIntro : MarkdownTest :::: """; - [Test, DisplayName("reports a link to a page that does not exist")] - public async Task ReportsMissingPage() => await Docs.HasError("does-not-exist"); + [Test, DisplayName("reports diagnostics")] + public async Task ReportsDiagnostics() => + await Docs.HasDiagnostics( + "error {repo}/docs/index.md:2 Directive link `/does-not-exist.md` does not exist. If it was recently removed add a redirect." + ); } public class CardGroupWithSolutionsVariant : MarkdownTest @@ -166,10 +254,13 @@ public class CardGroupWithSolutionsVariant : MarkdownTest :::: """; - [Test, DisplayName("locks the three column grid")] - public async Task LocksThreeColumnGrid() => - await Docs.ConvertsToContainingHtml( + [Test, DisplayName("renders HTML")] + public async Task RendersHtml() => + await Docs.ConvertsToHtml( """ +
    +

    Browse by area

    +
    • @@ -220,74 +311,12 @@ public class CardGroupNestedInExplore : MarkdownTest ::::: """; - [Test, DisplayName("renders the explore heading")] - public async Task RendersExploreHeading() => - await Docs.ConvertsToContainingHtml( - """ -
      -

      Explore the docs

      -

      Organized by task.

      -
      - """ - ); - - // One snapshot covers what nesting changes: accordion mode, the first accordion open and + // The snapshot covers what nesting changes: accordion mode, the first accordion open and // the rest closed, and link cards as columns. It also pins the heading levels, so an - // Explore stack keeps a complete outline: section h2, accordion h3, column h4. - [Test, DisplayName("renders the accordion stack")] - public async Task RendersAccordionStack() => - await Docs.ConvertsToContainingHtml( - """ -
      - -
      - -

      Authoring

      - -
      -
      -
        -
      • -

        - Syntax -

        -
      • -
      -
      -
      -
      - """ - ); - - // A column is a pure link index, so the description is deliberately dropped. - [Test, DisplayName("drops the description in column mode")] - public async Task DropsDescriptionInColumnMode() => - await Docs.DoesNotConvertToContainingHtml("This description is dropped in column mode."); + // Explore stack keeps a complete outline: section h2, accordion h3, column h4. A column is a + // pure link index, so the description is deliberately dropped. + [Test, DisplayName("renders HTML")] + public async Task RendersHtml() => await Docs.ConvertsToHtmlSnapshot(); [Test, DisplayName("has no errors")] public async Task HasNoErrors() => await Docs.HasNoErrors(); @@ -322,10 +351,13 @@ public class ExploreCollapsedByDefault : MarkdownTest """; // Without `:mode:`, every accordion starts closed, so neither `
      ` carries `open`. - [Test, DisplayName("renders every accordion collapsed")] - public async Task RendersEveryAccordionCollapsed() => - await Docs.ConvertsToContainingHtml( + [Test, DisplayName("renders HTML")] + public async Task RendersHtml() => + await Docs.ConvertsToHtml( """ +
      +

      Explore the docs

      +

    "); + [Test, DisplayName("renders HTML")] + public async Task RendersHtml() => + await Docs.ConvertsToHtml( + """ +
    +
    +
    +

    Changed panel

    +
    +
    +
      +
    • + +
    • +
    +
    + """ + ); [Test] public async Task HasNoErrors() => await Docs.HasNoErrors(); diff --git a/tests/Elastic.Authoring.Tests/Blocks/ImageBlocks.cs b/tests/Elastic.Authoring.Tests/Blocks/ImageBlocks.cs index 582feb542e..972efc585f 100644 --- a/tests/Elastic.Authoring.Tests/Blocks/ImageBlocks.cs +++ b/tests/Elastic.Authoring.Tests/Blocks/ImageBlocks.cs @@ -17,11 +17,25 @@ public class StaticPathToImage : MarkdownTest ::: """; - [Test, DisplayName("validate src is anchored")] - public async Task ValidateSrcIsAnchored() => - await Docs.ConvertsToContainingHtml( + [Test, DisplayName("renders HTML")] + public async Task RendersHtml() => + await Docs.ConvertsToHtml( """ - Elasticsearch + + Elasticsearch + + """ ); } @@ -55,20 +69,48 @@ public class SupportsUrlPathPrefix : GeneratorTest ) ]; - [Test, DisplayName("validate image src contains prefix")] - public async Task ValidateImageSrcContainsPrefix() => - await Docs.ConvertsToContainingHtml( + [Test, DisplayName("renders HTML")] + public async Task RendersHtml() => + await Docs.ConvertsToHtml( """ - Elasticsearch + + Elasticsearch + + """ ); - [Test, DisplayName("validate image src contains prefix when referenced relatively")] - public async Task ValidateRelativeReference() => - await Docs.Converts("folder/relative.md").ContainsHtml( + [Test, DisplayName("renders HTML")] + public async Task RendersHtmlOfFolderRelative() => + await Docs.Converts("folder/relative.md").ToHtml( """ + Elasticsearch - """ + + + """ ); [Test, DisplayName("has no errors")] @@ -94,16 +136,36 @@ public class ImageRefOutOfScope : GeneratorTest ) ]; - [Test, DisplayName("validate image src contains prefix and is anchored to documentation scope root")] - public async Task ValidateSrcAnchored() => - await Docs.ConvertsToContainingHtml( + [Test, DisplayName("renders HTML")] + public async Task RendersHtml() => + await Docs.ConvertsToHtml( """ - Elasticsearch + + Elasticsearch + + """ ); - [Test, DisplayName("emits an error image reference is outside of documentation scope")] - public async Task EmitsError() => await Docs.HasError("./img/observability.png` does not exist. resolved to"); + [Test, DisplayName("reports diagnostics")] + public async Task ReportsDiagnostics() => + await Docs.HasDiagnostics( + """ + error {repo}/docs/index.md:2 `../img/observability.png` does not exist. resolved to `Nullean.ScopedFileSystem.ScopedFileInfo + warning {repo}/docs/index.md:2 Image '../img/observability.png' is referenced out of table of contents scope '{repo}/docs'. + """ + ); } public class ScreenshotClassInsideListInAppliesSwitch : MarkdownTest @@ -125,8 +187,42 @@ 1. Customize the lifecycle. ::::: """; - [Test, DisplayName("keeps screenshot class when the image sits in a list")] - public async Task KeepsScreenshotClassInsideList() => await Docs.ConvertsToContainingRawHtml("class=\"screenshot\""); + [Test, DisplayName("renders HTML")] + public async Task RendersHtml() => + await Docs.ConvertsToHtml( + """ +
    + + +
    +
      +
    1. +

      Customize the lifecycle.

      + + Elasticsearch + + +
    2. +
    +
    +
    + """ + ); } public class EmptyAltText : MarkdownTest @@ -138,9 +234,25 @@ public class EmptyAltText : MarkdownTest ::: """; - [Test, DisplayName("validate empty alt attribute")] - public async Task ValidateEmptyAlt() => - await Docs.ConvertsToContainingHtml(""" - - """); + [Test, DisplayName("renders HTML")] + public async Task RendersHtml() => + await Docs.ConvertsToHtml( + """ + + + + + """ + ); } diff --git a/tests/Elastic.Authoring.Tests/Blocks/NestedDirectiveOptions.cs b/tests/Elastic.Authoring.Tests/Blocks/NestedDirectiveOptions.cs index fd36c39b64..8712b8d8fb 100644 --- a/tests/Elastic.Authoring.Tests/Blocks/NestedDirectiveOptions.cs +++ b/tests/Elastic.Authoring.Tests/Blocks/NestedDirectiveOptions.cs @@ -33,15 +33,25 @@ container body """; // The group is declared on the tab-set before any child, so it still reaches the tab-set. - [Test, DisplayName("the tab set keeps its own group")] - public async Task TabSetKeepsGroup() => await Docs.ConvertsToContainingRawHtml("data-sync-group=\"install-method\""); - // Each sync reaches the item that declared it, rather than all landing on the last one. - [Test, DisplayName("the first item keeps its own sync")] - public async Task FirstItemKeepsSync() => await Docs.ConvertsToContainingRawHtml("data-sync-id=\"local\""); - - [Test, DisplayName("the second item keeps its own sync")] - public async Task SecondItemKeepsSync() => await Docs.ConvertsToContainingRawHtml("data-sync-id=\"container\""); + [Test, DisplayName("renders HTML")] + public async Task RendersHtml() => + await Docs.ConvertsToHtml( + """ +
    + + +
    +

    local body

    +
    + + +
    +

    container body

    +
    +
    + """ + ); [Test, DisplayName("has no errors")] public async Task HasNoErrors() => await Docs.HasNoErrors(); @@ -66,14 +76,32 @@ serverless body :::: """; - [Test, DisplayName("the switch keeps its own group")] - public async Task SwitchKeepsGroup() => await Docs.ConvertsToContainingRawHtml("data-sync-group=\"deployment\""); - - [Test, DisplayName("the first item keeps its own sync")] - public async Task FirstItemKeepsSync() => await Docs.ConvertsToContainingRawHtml("data-sync-id=\"serverless\""); - - [Test, DisplayName("the second item keeps its own sync")] - public async Task SecondItemKeepsSync() => await Docs.ConvertsToContainingRawHtml("data-sync-id=\"self-managed\""); + [Test, DisplayName("renders HTML")] + public async Task RendersHtml() => + await Docs.ConvertsToHtml( + """ +
    + + +
    +

    serverless body

    +
    + + +
    +

    self-managed body

    +
    +
    + """ + ); [Test, DisplayName("has no errors")] public async Task HasNoErrors() => await Docs.HasNoErrors(); @@ -95,11 +123,28 @@ Configure the thing. :::: """; - [Test, DisplayName("each step keeps its own anchor")] - public async Task EachStepKeepsAnchor() => await Docs.ConvertsToContainingRawHtml("install-step"); - - [Test, DisplayName("the second step keeps its own anchor")] - public async Task SecondStepKeepsAnchor() => await Docs.ConvertsToContainingRawHtml("configure-step"); + [Test, DisplayName("renders HTML")] + public async Task RendersHtml() => + await Docs.ConvertsToHtml( + """ +
    +
      +
    1. +

      + Install +

      +

      Install the thing.

      +
    2. +
    3. +

      + Configure +

      +

      Configure the thing.

      +
    4. +
    +
    + """ + ); [Test, DisplayName("has no errors")] public async Task HasNoErrors() => await Docs.HasNoErrors(); @@ -118,11 +163,34 @@ Inner content. :::: """; - [Test, DisplayName("the dropdown keeps its own open state")] - public async Task DropdownKeepsOpenState() => await Docs.ConvertsToContainingRawHtml("Outer summary"); - - [Test, DisplayName("the nested admonition keeps its own name")] - public async Task NestedAdmonitionKeepsName() => await Docs.ConvertsToContainingRawHtml("inner-note"); + [Test, DisplayName("renders HTML")] + public async Task RendersHtml() => + await Docs.ConvertsToHtml( + """ + + """ + ); [Test, DisplayName("has no errors")] public async Task HasNoErrors() => await Docs.HasNoErrors(); diff --git a/tests/Elastic.Authoring.Tests/Blocks/PageCards.cs b/tests/Elastic.Authoring.Tests/Blocks/PageCards.cs index 107c22b7d6..ccf60912f0 100644 --- a/tests/Elastic.Authoring.Tests/Blocks/PageCards.cs +++ b/tests/Elastic.Authoring.Tests/Blocks/PageCards.cs @@ -17,9 +17,9 @@ public class PageCardWithARelativeLink : MarkdownTest ::: """; - [Test, DisplayName("resolves the link relative to the source file")] - public async Task ResolvesLinkRelative() => - await Docs.ConvertsToContainingHtml( + [Test, DisplayName("renders HTML")] + public async Task RendersHtml() => + await Docs.ConvertsToHtml( """
    @@ -70,8 +70,11 @@ public class PageCardWithAnAbsoluteUrl : MarkdownTest ::: """; - [Test, DisplayName("errors")] - public async Task Errors() => await Docs.HasError("page-card url must be a local .md path or crosslink"); + [Test, DisplayName("reports diagnostics")] + public async Task ReportsDiagnostics() => + await Docs.HasDiagnostics( + "error {repo}/docs/index.md:2 page-card url must be a local .md path or crosslink, not an absolute URL: https://www.elastic.co" + ); } public class PageCardWithoutAMarkdownLink : MarkdownTest @@ -81,6 +84,7 @@ public class PageCardWithoutAMarkdownLink : MarkdownTest ::: """; - [Test, DisplayName("errors")] - public async Task Errors() => await Docs.HasError("page-card requires a markdown link argument"); + [Test, DisplayName("reports diagnostics")] + public async Task ReportsDiagnostics() => + await Docs.HasDiagnostics("error {repo}/docs/index.md:2 page-card requires a markdown link argument: [Title](url)"); } diff --git a/tests/Elastic.Authoring.Tests/Blocks/Storybook.cs b/tests/Elastic.Authoring.Tests/Blocks/Storybook.cs index 2139edca7c..e12b9bd407 100644 --- a/tests/Elastic.Authoring.Tests/Blocks/Storybook.cs +++ b/tests/Elastic.Authoring.Tests/Blocks/Storybook.cs @@ -11,8 +11,9 @@ public class StorybookMissingReference : MarkdownTest ::: """; - [Test, DisplayName("has error")] - public async Task HasError() => await Docs.HasError("requires :id: or :project:"); + [Test, DisplayName("reports diagnostics")] + public async Task ReportsDiagnostics() => + await Docs.HasDiagnostics("error {repo}/docs/index.md:2 storybook directive requires :id: or :project:."); } public class StorybookMissingRegistry : MarkdownTest @@ -24,6 +25,7 @@ public class StorybookMissingRegistry : MarkdownTest ::: """; - [Test, DisplayName("has error")] - public async Task HasError() => await Docs.HasError("requires docset.yml storybook.registry"); + [Test, DisplayName("reports diagnostics")] + public async Task ReportsDiagnostics() => + await Docs.HasDiagnostics("error {repo}/docs/index.md:2 storybook directive requires docset.yml storybook.registry."); } diff --git a/tests/Elastic.Authoring.Tests/DesignSystem/DesignSystemDirectives.cs b/tests/Elastic.Authoring.Tests/DesignSystem/DesignSystemDirectives.cs index 3bfc00e1cb..648a64ef1e 100644 --- a/tests/Elastic.Authoring.Tests/DesignSystem/DesignSystemDirectives.cs +++ b/tests/Elastic.Authoring.Tests/DesignSystem/DesignSystemDirectives.cs @@ -24,16 +24,24 @@ Back up first. ::: """; - [Test, DisplayName("renders the warning admonition component")] - public async Task Renders() - { - await Docs.ConvertsToContainingRawHtml("
    "); - await Docs.ConvertsToContainingRawHtml("Warning"); - await Docs.ConvertsToContainingRawHtml("

    Back up first.

    "); - } - - [Test, DisplayName("does not emit legacy admonition markup")] - public async Task NoLegacyMarkup() => await Docs.DoesNotConvertToContainingHtml("admonition-header"); + [Test, DisplayName("renders HTML")] + public async Task RendersHtml() => + await Docs.ConvertsToHtml( + """ +
    +
    + + + + + Warning +
    +
    +

    Back up first.

    +
    +
    + """ + ); [Test, DisplayName("has no errors")] public async Task HasNoErrors() => await Docs.HasNoErrors(); @@ -48,11 +56,29 @@ public class AdmonitionInListStaysNested : DesignSystemDirectiveTest ::::: """; - [Test, DisplayName("keeps the admonition inside the list item")] - public async Task Renders() => await Docs.ConvertsToContainingRawHtml("
  • List Item 1"); - - [Test, DisplayName("renders the note component")] - public async Task NoteComponent() => await Docs.ConvertsToContainingRawHtml("class=\"admonition admonition--note\""); + [Test, DisplayName("renders HTML")] + public async Task RendersHtml() => + await Docs.ConvertsToHtml( + """ +
      +
    • List Item 1 +
      +
      + + + + + + Note +
      +
      +

      Hello, World!

      +
      +
      +
    • +
    + """ + ); } public class DropdownComponent : DesignSystemDirectiveTest @@ -65,18 +91,25 @@ A unit of storage. ::: """; - [Test, DisplayName("renders a native details element that is open")] - public async Task Renders() => await Docs.ConvertsToContainingRawHtml("
    "); - await Docs.ConvertsToContainingRawHtml("What is a shard?"); - } - - [Test, DisplayName("renders the body")] - public async Task Body() => await Docs.ConvertsToContainingRawHtml("

    A unit of storage.

    "); + [Test, DisplayName("renders HTML")] + public async Task RendersHtml() => + await Docs.ConvertsToHtml( + """ + + """ + ); } public class TabsComponent : DesignSystemDirectiveTest @@ -98,27 +131,24 @@ Use Python. :::: """; - [Test, DisplayName("renders radio tabs with sync attributes")] - public async Task Renders() - { - await Docs.ConvertsToContainingRawHtml("class=\"tab-set\""); - await Docs.ConvertsToContainingRawHtml("data-tab-sync-key=\"curl\""); - await Docs.ConvertsToContainingRawHtml("data-tab-sync-group=\"lang\""); - } - - [Test, DisplayName("checks only the first tab")] - public async Task FirstChecked() - { - await Docs.ConvertsToContainingRawHtml("id=\"tabs-item-1-0\" name=\"tabs-set-1\" checked>"); - await Docs.ConvertsToContainingRawHtml("id=\"tabs-item-1-1\" name=\"tabs-set-1\">"); - } - - [Test, DisplayName("does not use the legacy tab classes the old script binds to")] - public async Task NoLegacyHooks() - { - await Docs.DoesNotConvertToContainingHtml("tabs-label"); - await Docs.DoesNotConvertToContainingHtml("data-sync-id"); - } + [Test, DisplayName("renders HTML")] + public async Task RendersHtml() => + await Docs.ConvertsToHtml( + """ +
    + + +
    +

    Use curl.

    +
    + + +
    +

    Use Python.

    +
    +
    + """ + ); } public class StepperComponent : DesignSystemDirectiveTest @@ -136,16 +166,28 @@ Run it. :::: """; - [Test, DisplayName("renders an ordered list of steps")] - public async Task Renders() - { - await Docs.ConvertsToContainingRawHtml("
      "); - await Docs.ConvertsToContainingRawHtml("
    1. "); - } - - [Test, DisplayName("renders step titles as headings with anchors")] - public async Task Titles() => - await Docs.ConvertsToContainingRawHtml("

      Install

      "); + [Test, DisplayName("renders HTML")] + public async Task RendersHtml() => + await Docs.ConvertsToHtml( + """ +
      +
        +
      1. +

        + Install +

        +

        Download it.

        +
      2. +
      3. +

        + Start +

        +

        Run it.

        +
      4. +
      +
      + """ + ); } public class ButtonDirectiveComponent : DesignSystemDirectiveTest @@ -156,12 +198,17 @@ [Get started](https://www.elastic.co) ::: """; - [Test, DisplayName("wraps the link in the button directive")] - public async Task Renders() - { - await Docs.ConvertsToContainingRawHtml("class=\"button-directive button-directive--primary"); - await Docs.ConvertsToContainingRawHtml("Get started"); - } + [Test, DisplayName("renders HTML")] + public async Task RendersHtml() => + await Docs.ConvertsToHtml( + """ +
      +

      + Get started +

      +
      + """ + ); } /// Flag off must keep producing the legacy markup, whatever the design system adds. @@ -178,14 +225,35 @@ Back up first. ::: """; - [Test, DisplayName("uses the legacy admonition and dropdown")] - public async Task Legacy() - { - await Docs.ConvertsToContainingRawHtml("class=\"admonition warning"); - await Docs.ConvertsToContainingRawHtml("class=\"dropdown"); - await Docs.DoesNotConvertToContainingHtml("admonition__"); - await Docs.DoesNotConvertToContainingHtml("disclosure"); - } + [Test, DisplayName("renders HTML")] + public async Task RendersHtml() => + await Docs.ConvertsToHtml( + """ +
      +
      + Warning +
      +
      +

      Back up first.

      +
      +
      + + """ + ); } public class CodeBlockComponent : DesignSystemDirectiveTest @@ -203,30 +271,42 @@ public class CodeBlockComponent : DesignSystemDirectiveTest 1. The cluster name. """; - [Test, DisplayName("renders the code block component with the language label")] - public async Task Renders() - { - await Docs.ConvertsToContainingRawHtml("
      "); - await Docs.ConvertsToContainingRawHtml("JSON"); - await Docs.ConvertsToContainingRawHtml("
      ");
      -	}
      -
      -	[Test, DisplayName("keeps pre > code so the highlighter finds the code")]
      -	public async Task KeepsPreCode() => await Docs.ConvertsToContainingRawHtml("");
      -
      -	[Test, DisplayName("renders a copy button that stays hidden until the script reveals it")]
      -	public async Task CopyButton() =>
      -		await Docs.ConvertsToContainingRawHtml("
      +			
      +			
      +				{ "size": 10 }
      +				
      +			
      +
      +
      +
      + YAML + +
      +
      +				cluster.name: docs
      +					
      +				
      +			
      +
      +
        +
      1. The cluster name.
      2. +
      + """ + ); } public class PageShellComponents : DesignSystemDirectiveTest @@ -314,6 +394,29 @@ public class ConsoleCodeBlockSegments : DesignSystemDirectiveTest ``` """; + [Test, DisplayName("renders HTML")] + public async Task RendersHtml() => + await Docs.ConvertsToHtml( + """ +
      +
      + JSON + +
      +
      +				GET /_search
      +				{ "size": 1 }
      +				POST /docs/_doc
      +				{ "title": "one" }
      +			
      +
      + """ + ); + + // Inside
       a newline between two elements is output, which a pretty-printed snapshot cannot show.
       	[Test, DisplayName("starts each request and each body on its own line")]
       	public async Task SegmentsAreSeparatedByANewline()
       	{
      @@ -335,37 +438,8 @@ This applies to the stack.
       		Inline badge: {applies_to}`stack: preview 9.0`
       		""";
       
      -	[Test, DisplayName("renders the design-system badge instead of the React element")]
      -	public async Task Badge()
      -	{
      -		await Docs.ConvertsToContainingRawHtml("class=\"applies-badge applies-badge--ga");
      -		await Docs.ConvertsToContainingRawHtml("class=\"applies-badge__product\">Stack");
      -		await Docs.DoesNotConvertToContainingHtml("");
      -		await Docs.ConvertsToContainingRawHtml("data-applies-trigger");
      -		await Docs.ConvertsToContainingRawHtml("applies-popover__trigger");
      -	}
      -
      -	[Test, DisplayName("shows the lifecycle text when the version is not released yet")]
      -	public async Task Status()
      -	{
      -		// The documented versions are ahead of the test version config, so the badge shows its lifecycle text instead of a version.
      -		await Docs.ConvertsToContainingRawHtml("Planned");
      -		await Docs.ConvertsToContainingRawHtml("applies-badge--preview");
      -	}
      +	[Test, DisplayName("renders HTML")]
      +	public async Task RendersHtml() => await Docs.ConvertsToHtmlSnapshot();
       }
       
       public class VectorSizingComponent : DesignSystemDirectiveTest
      @@ -375,20 +449,8 @@ public class VectorSizingComponent : DesignSystemDirectiveTest
       		:::
       		""";
       
      -	[Test, DisplayName("renders the native form instead of the React element")]
      -	public async Task Renders()
      -	{
      -		await Docs.ConvertsToContainingRawHtml("