From bb7a78520149b17c714138ce6cd6248a4efec922 Mon Sep 17 00:00:00 2001 From: lcawl Date: Wed, 9 Sep 2026 15:56:22 -0500 Subject: [PATCH 1/2] Render features and enhancements separately in release notes --- docs/changelog/bundles/0.100.0.yaml | 2 +- docs/syntax/changelog.md | 45 ++- .../ReleaseNotes/ChangelogEntryType.cs | 2 +- .../Directives/Changelog/ChangelogBlock.cs | 97 +++++-- .../ChangelogDescriptionVisibility.cs | 28 +- .../Changelog/ChangelogInlineRenderer.cs | 43 ++- .../Asciidoc/ChangelogAsciidocRenderer.cs | 15 +- .../Markdown/ChangelogGfmRenderer.cs | 20 +- .../Markdown/IndexMarkdownRenderer.cs | 28 +- .../Changelogs/Render/GfmRenderTests.cs | 12 +- .../Changelogs/Render/OutputFormatTests.cs | 8 +- .../Directives/ChangelogBasicTests.cs | 12 +- .../Directives/ChangelogConfigTests.cs | 2 +- .../ChangelogDescriptionVisibilityTests.cs | 256 +++++++++++++++++- ...ChangelogFeatureEnhancementSectionTests.cs | 145 ++++++++++ .../Directives/ChangelogHideLinksTests.cs | 2 +- .../ChangelogHighlightsOptionTests.cs | 10 +- .../Directives/ChangelogTocFilteringTests.cs | 20 +- .../Directives/ChangelogTypeFilterTests.cs | 20 +- 19 files changed, 638 insertions(+), 129 deletions(-) create mode 100644 tests/Elastic.Markdown.Tests/Directives/ChangelogFeatureEnhancementSectionTests.cs diff --git a/docs/changelog/bundles/0.100.0.yaml b/docs/changelog/bundles/0.100.0.yaml index 98a9b225f7..2281c7d9a6 100644 --- a/docs/changelog/bundles/0.100.0.yaml +++ b/docs/changelog/bundles/0.100.0.yaml @@ -2,7 +2,7 @@ products: - product: docs-builder target: 0.100.0 entries: - # Features and enhancements + # Features - type: feature title: Add PR label blockers to changelog creation products: diff --git a/docs/syntax/changelog.md b/docs/syntax/changelog.md index f06ff051c9..fbe27415d0 100644 --- a/docs/syntax/changelog.md +++ b/docs/syntax/changelog.md @@ -25,7 +25,7 @@ The directive supports the following options: | `:type: value` | Filter entries by type | Excludes separated types | | `:subsections:` | Group entries by area/component | false | | `:link-visibility: value` | Visibility of pull request (PR) and issue links | `auto` | -| `:description-visibility: value` | Visibility of changelog **record** descriptions (YAML `description` on each entry) | `auto` | +| `:description-visibility: value[, value…]` | Visibility of changelog descriptions (YAML `description` on each entry). Comma-separated tokens are combined. | `auto` | | `:dropdowns:` | Render breaking changes, deprecations, known issues, and highlights as expandable dropdowns instead of flattened bulleted lists | false | | `:highlights:` | Emit a dedicated highlights section for entries with `highlight: true` (entries still appear under their type sections) | false | | `:release-dates:` | Render the bundle `release-date` field as _Released: …_ after the version heading | false | @@ -124,16 +124,32 @@ This aligns with the `changelog render` command's link visibility controls. #### `:description-visibility:` -Controls whether the **`description`** text on each **changelog record** appears in output (bullet body text under each item, or the first paragraph inside a breaking-change, deprecation, known-issue, or highlight entry when [`:dropdowns:`](#dropdowns) is enabled). This is **different** from the optional **bundle** `description` field (release intro prose after `_Released:_`), which is always shown when present. See [Rendered output](#rendered-output). +Controls whether the `description` text for each changelog appears in the output (bullet body text under each item, or the first paragraph inside a breaking-change, deprecation, known-issue, or highlight entry when [`:dropdowns:`](#dropdowns) is enabled). +This is different from the optional bundle `description` field (release intro prose after `_Released:_`), which is always shown when present. +See [Rendered output](#rendered-output). + +Pass one or more comma-separated tokens. +There is at most one _base token_ (`auto`, `keep-descriptions`, or `hide-descriptions`). +_Overlay tokens_ (`keep-highlight-descriptions`, `keep-feature-descriptions`) combine with each other and with a base. | Value | Behavior | |-------|----------| -| `auto` | When **every** constituent repository in the bundle’s resolved repo identity is **public** (same private-repo detection as `:link-visibility:` from `assembler.yml`, including `repo1+repo2` merged bundles), **omit** record `description` bodies. When **any** constituent is marked **private**, **show** those bodies. In standalone builds without `assembler.yml`, every repo is treated as public ⇒ changelog descriptions are omitted under `auto`. | +| `auto` | When every constituent repository in the bundle’s resolved repo identity is public (same private-repo detection as `:link-visibility:` from `assembler.yml`, including `repo1+repo2` merged bundles), omit record `description` bodies. When any constituent is marked private, show those bodies. In standalone builds without `assembler.yml`, every repo is treated as public ⇒ changelog descriptions are omitted under `auto`. Overlay tokens still show descriptions in their sections. | | `keep-descriptions` | Always render changelog descriptions when present in the bundle source. Use this on pages such as deprecations or breaking changes when you still want full release-note prose alongside public repos. | -| `keep-highlight-descriptions` | Show changelog descriptions **only** in the [`:highlights:`](#highlights) section. Hide them in every other section, including the type-section copy of a highlighted entry. When `:highlights:` is omitted, descriptions are hidden everywhere. | -| `hide-descriptions` | Always omit changelog descriptions (titles, PR/issue links, impact, and action sections and bundle-level intros are unaffected). | +| `keep-highlight-descriptions` | Overlay: always show changelog descriptions in the [Highlights](#highlights) section. When used alone, hide them in every other section, including the type-section copy of a highlighted entry. When `:highlights:` is omitted, the overlay has no visible effect. | +| `keep-feature-descriptions` | Overlay: always show changelog descriptions in the "Features" section. When used alone, hide them in every other section. | +| `hide-descriptions` | Omit changelog descriptions in unmarked sections (titles, PR/issue links, impact, and action sections and bundle-level intros are unaffected). Overlay tokens still show descriptions in their sections. | + +```markdown +:::{changelog} +:highlights: +:description-visibility: keep-feature-descriptions, keep-highlight-descriptions +::: +``` + +The example shows prose in Features and Highlights, and hides it in Enhancements, Fixes, and other type sections. -**Contrast with `:link-visibility:`:** `:link-visibility: auto` hides **links** when a repo is **private**. `:description-visibility: auto` **shows** richer record **description** prose when **any** source repo is **private**, and hides that prose for bundles that resolve to **only public** repositories. +**Contrast with `:link-visibility:`:** `:link-visibility: auto` hides **links** when a repo is **private**. `:description-visibility: auto` **shows** richer record **description** prose when **any** source repo is **private**, and hides that prose for bundles that resolve to **only public** repositories (except where an overlay token applies). #### `:dropdowns:` [dropdowns] @@ -154,12 +170,12 @@ Controls whether entries with `highlight: true` get a dedicated **Highlights** s | Mode | Behavior | |------|----------| -| (omitted, default) | Inline only: highlighted entries appear under their normal type sections (for example Features and enhancements). No `### Highlights` section. | +| (omitted, default) | Inline only: highlighted entries appear under their normal type sections (for example Features). No `### Highlights` section. | | `:highlights:` | Section: emit a `### Highlights` section and keep those entries under their type sections. | Use `:highlights:` on general release notes when you want a highlights section alongside features, enhancements, and bug fixes — without requiring `:type: all` (which also pulls in breaking changes, deprecations, and known issues). -Changelog descriptions follow [`:description-visibility:`](#description-visibility). Use `keep-descriptions` to show prose in every section, or `keep-highlight-descriptions` to show prose only in the highlights section. +Changelog descriptions follow [`:description-visibility:`](#description-visibility). Use `keep-descriptions` to show prose in every section, `keep-highlight-descriptions` for Highlights only, `keep-feature-descriptions` for Features only, or combine overlays: `keep-feature-descriptions, keep-highlight-descriptions`. #### `:release-dates:` [release-dates] @@ -379,13 +395,15 @@ This release includes new features and bug fixes. Download the release binaries: https://github.com/elastic/elasticsearch/releases/tag/v0.100.0 -### Features and enhancements +### Features +... +### Enhancements ... ### Fixes ... ## 2025-08-05 -### Features and enhancements +### Features ... ``` @@ -403,7 +421,8 @@ Each changelog entry may have its own `description` field in YAML (shown as body | Section | Entry type | Rendering | |---------|------------|-----------| -| Features and enhancements | `feature`, `enhancement` | Grouped by area | +| Features | `feature` | Grouped by area | +| Enhancements | `enhancement` | Grouped by area | | Fixes | `bug-fix`, `security` | Grouped by area | | Documentation | `docs` | Grouped by area | | Regressions | `regression` | Grouped by area | @@ -416,12 +435,14 @@ Each changelog entry may have its own `description` field in YAML (shown as body **Note about highlights:** - The highlights section appears only when [`:highlights:`](#highlights) is set and at least one entry has `highlight: true` -- When the section is shown, highlighted entries appear in **both** the highlights section and their original type section (for example, both the "highlights" and "features and enhancements" sections) +- When the section is shown, highlighted entries appear in **both** the highlights section and their original type section (for example, both the "highlights" and "features" sections) - When `:highlights:` is omitted, flagged entries still appear under their type sections (inline only) - You can combine `:highlights:` with the default type filter (no `:type:`) to show highlights alongside features and fixes without including breaking changes, deprecations, or known issues Sections with no entries of that type are omitted from the output. Releases with no entries after the `:type:` filter are omitted entirely, except on general release notes (`:type: all` or default) when the bundle has a `description`. +Section heading anchors use `-features` and `-enhancements` suffixes. Pages that previously linked to `-features-enhancements` need those links updated. + ## Error behavior for invalid entries [changelog-missing-files] Bundles are self-contained: every entry must embed its content (`title`, `type`, and so on) inline. diff --git a/src/Elastic.Documentation/ReleaseNotes/ChangelogEntryType.cs b/src/Elastic.Documentation/ReleaseNotes/ChangelogEntryType.cs index 32ce2ae362..c624e11a1b 100644 --- a/src/Elastic.Documentation/ReleaseNotes/ChangelogEntryType.cs +++ b/src/Elastic.Documentation/ReleaseNotes/ChangelogEntryType.cs @@ -17,7 +17,7 @@ public enum ChangelogEntryType [Display(Name = "invalid")] Invalid = 0, - /// A new feature or enhancement. + /// A new capability that did not exist before. [Display(Name = "feature")] Feature, diff --git a/src/Elastic.Markdown/Myst/Directives/Changelog/ChangelogBlock.cs b/src/Elastic.Markdown/Myst/Directives/Changelog/ChangelogBlock.cs index 5caa9a86cf..ca073d7936 100644 --- a/src/Elastic.Markdown/Myst/Directives/Changelog/ChangelogBlock.cs +++ b/src/Elastic.Markdown/Myst/Directives/Changelog/ChangelogBlock.cs @@ -284,23 +284,74 @@ private ChangelogDescriptionVisibility ParseDescriptionVisibility() if (string.IsNullOrWhiteSpace(value)) return ChangelogDescriptionVisibility.Auto; - return value.ToLowerInvariant() switch + var result = ChangelogDescriptionVisibility.None; + var sawBase = false; + var hadValid = false; + + foreach (var raw in value.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) + hadValid |= TryAccumulateDescriptionVisibilityToken(raw, ref result, ref sawBase); + + return hadValid ? result : ChangelogDescriptionVisibility.Auto; + } + + private bool TryAccumulateDescriptionVisibilityToken(string raw, ref ChangelogDescriptionVisibility result, ref bool sawBase) + { + if (!TryMapDescriptionVisibilityToken(raw, out var token, out var isBase)) { - "auto" => ChangelogDescriptionVisibility.Auto, - "keep-descriptions" => ChangelogDescriptionVisibility.KeepDescriptions, - "keep-highlight-descriptions" => ChangelogDescriptionVisibility.KeepHighlightDescriptions, - "hide-descriptions" => ChangelogDescriptionVisibility.HideDescriptions, - _ => EmitInvalidDescriptionVisibilityWarning(value) - }; + EmitInvalidDescriptionVisibilityWarning(raw); + return false; + } + + if (isBase && sawBase) + { + this.EmitWarning( + $"Multiple :description-visibility: base values; ignoring '{raw}'. Valid base values are: auto, keep-descriptions, hide-descriptions." + ); + return true; + } + + if (isBase) + sawBase = true; + + result |= token; + return true; } - private ChangelogDescriptionVisibility EmitInvalidDescriptionVisibilityWarning(string value) + private static bool TryMapDescriptionVisibilityToken(string raw, out ChangelogDescriptionVisibility token, out bool isBase) { + switch (raw.ToLowerInvariant()) + { + case "auto": + token = ChangelogDescriptionVisibility.Auto; + isBase = true; + return true; + case "keep-descriptions": + token = ChangelogDescriptionVisibility.KeepDescriptions; + isBase = true; + return true; + case "hide-descriptions": + token = ChangelogDescriptionVisibility.HideDescriptions; + isBase = true; + return true; + case "keep-highlight-descriptions": + token = ChangelogDescriptionVisibility.KeepHighlightDescriptions; + isBase = false; + return true; + case "keep-feature-descriptions": + token = ChangelogDescriptionVisibility.KeepFeatureDescriptions; + isBase = false; + return true; + default: + token = ChangelogDescriptionVisibility.None; + isBase = false; + return false; + } + } + + private void EmitInvalidDescriptionVisibilityWarning(string value) => this.EmitWarning( - $"Invalid :description-visibility: value '{value}'. Valid values are: auto, keep-descriptions, keep-highlight-descriptions, hide-descriptions. Using auto." + $"Invalid :description-visibility: value '{value}'. Valid values are: auto, keep-descriptions, keep-highlight-descriptions, keep-feature-descriptions, hide-descriptions. Using auto when no valid tokens remain." ); - return ChangelogDescriptionVisibility.Auto; - } /// /// Parses and validates the :type: option. @@ -663,12 +714,13 @@ private IEnumerable ComputeGeneratedAnchors() ) yield return $"{repo}-{anchorSlug}-deprecations"; + if (!dedicatedPage && shouldInclude(ChangelogEntryType.Feature) && entriesByType.ContainsKey(ChangelogEntryType.Feature)) + yield return $"{repo}-{anchorSlug}-features"; + if ( - !dedicatedPage - && shouldInclude(ChangelogEntryType.Feature) - && (entriesByType.ContainsKey(ChangelogEntryType.Feature) || entriesByType.ContainsKey(ChangelogEntryType.Enhancement)) + !dedicatedPage && shouldInclude(ChangelogEntryType.Enhancement) && entriesByType.ContainsKey(ChangelogEntryType.Enhancement) ) - yield return $"{repo}-{anchorSlug}-features-enhancements"; + yield return $"{repo}-{anchorSlug}-enhancements"; if (!dedicatedPage && shouldInclude(ChangelogEntryType.BugFix) && entriesByType.ContainsKey(ChangelogEntryType.BugFix)) yield return $"{repo}-{anchorSlug}-fixes"; @@ -755,16 +807,11 @@ private IEnumerable ComputeTableOfContent() if (shouldInclude(ChangelogEntryType.Deprecation) && entriesByType.ContainsKey(ChangelogEntryType.Deprecation)) yield return new PageTocItem { Heading = "Deprecations", Slug = SectionSlug("deprecations"), Level = 3 }; - if ( - shouldInclude(ChangelogEntryType.Feature) - && (entriesByType.ContainsKey(ChangelogEntryType.Feature) || entriesByType.ContainsKey(ChangelogEntryType.Enhancement)) - ) - yield return new PageTocItem - { - Heading = "Features and enhancements", - Slug = SectionSlug("features-enhancements"), - Level = 3 - }; + if (shouldInclude(ChangelogEntryType.Feature) && entriesByType.ContainsKey(ChangelogEntryType.Feature)) + yield return new PageTocItem { Heading = "Features", Slug = SectionSlug("features"), Level = 3 }; + + if (shouldInclude(ChangelogEntryType.Enhancement) && entriesByType.ContainsKey(ChangelogEntryType.Enhancement)) + yield return new PageTocItem { Heading = "Enhancements", Slug = SectionSlug("enhancements"), Level = 3 }; if (shouldInclude(ChangelogEntryType.BugFix) && entriesByType.ContainsKey(ChangelogEntryType.BugFix)) yield return new PageTocItem { Heading = "Fixes", Slug = SectionSlug("fixes"), Level = 3 }; diff --git a/src/Elastic.Markdown/Myst/Directives/Changelog/ChangelogDescriptionVisibility.cs b/src/Elastic.Markdown/Myst/Directives/Changelog/ChangelogDescriptionVisibility.cs index 00b51272ca..fcfda6631a 100644 --- a/src/Elastic.Markdown/Myst/Directives/Changelog/ChangelogDescriptionVisibility.cs +++ b/src/Elastic.Markdown/Myst/Directives/Changelog/ChangelogDescriptionVisibility.cs @@ -6,29 +6,41 @@ namespace Elastic.Markdown.Myst.Directives.Changelog; /// /// Controls changelog entry description (body text) rendering for the {changelog} directive. -/// Mirrors the structure of while using opposite privacy defaults for . +/// Combinable flags: at most one base token (, , or +/// ) plus optional overlays (, +/// ). /// +[Flags] public enum ChangelogDescriptionVisibility { + None = 0, + /// /// Hide record descriptions when the bundle has only public constituent repos (per assembler.yml); /// show when any constituent is private. With no private repos configured, hides descriptions everywhere. /// - Auto, + Auto = 1, /// /// Always render record descriptions when present in source YAML. /// - KeepDescriptions, + KeepDescriptions = 1 << 1, + + /// + /// Never render record descriptions (including dropdown authoring placeholders), except where an overlay applies. + /// + HideDescriptions = 1 << 2, /// - /// Always render record descriptions in the Highlights section only; hide them in every other section. - /// When :highlights: is omitted, descriptions are hidden everywhere. + /// Always render record descriptions in the Highlights section. Combine with other tokens to control remaining sections. + /// When used alone, descriptions are hidden everywhere except Highlights. When :highlights: is omitted, the + /// Highlights overlay has no visible effect. /// - KeepHighlightDescriptions, + KeepHighlightDescriptions = 1 << 3, /// - /// Never render record descriptions (including dropdown authoring placeholders). + /// Always render record descriptions in the Features section. Combine with other tokens to control remaining sections. + /// When used alone, descriptions are hidden everywhere except Features. /// - HideDescriptions + KeepFeatureDescriptions = 1 << 4 } diff --git a/src/Elastic.Markdown/Myst/Directives/Changelog/ChangelogInlineRenderer.cs b/src/Elastic.Markdown/Myst/Directives/Changelog/ChangelogInlineRenderer.cs index ace3a5e704..49f75230d9 100644 --- a/src/Elastic.Markdown/Myst/Directives/Changelog/ChangelogInlineRenderer.cs +++ b/src/Elastic.Markdown/Myst/Directives/Changelog/ChangelogInlineRenderer.cs @@ -198,14 +198,25 @@ public static bool ShouldHideEntryDescriptionsForRepo( string bundleRepo, HashSet privateRepositories, ChangelogDescriptionVisibility visibility - ) => visibility switch + ) { - ChangelogDescriptionVisibility.HideDescriptions => true, - ChangelogDescriptionVisibility.KeepHighlightDescriptions => true, - ChangelogDescriptionVisibility.KeepDescriptions => false, - ChangelogDescriptionVisibility.Auto => !HasAnyPrivateRepoConstituent(bundleRepo, privateRepositories), - _ => !HasAnyPrivateRepoConstituent(bundleRepo, privateRepositories) - }; + if (visibility.HasFlag(ChangelogDescriptionVisibility.KeepDescriptions)) + return false; + + if (visibility.HasFlag(ChangelogDescriptionVisibility.HideDescriptions)) + return true; + + if (visibility.HasFlag(ChangelogDescriptionVisibility.Auto)) + return !HasAnyPrivateRepoConstituent(bundleRepo, privateRepositories); + + if ( + visibility.HasFlag(ChangelogDescriptionVisibility.KeepHighlightDescriptions) + || visibility.HasFlag(ChangelogDescriptionVisibility.KeepFeatureDescriptions) + ) + return true; + + return !HasAnyPrivateRepoConstituent(bundleRepo, privateRepositories); + } /// /// True when merged (elasticsearch+kibana-style) has at least one @@ -242,7 +253,9 @@ private static string GenerateMarkdown(BundleRenderModel model, ChangelogRenderO var subsections = options.Subsections; var hideLinks = model.HideLinks; var hideEntryDescriptions = model.HideEntryDescriptions; - var hideHighlightDescriptions = options.DescriptionVisibility is not ChangelogDescriptionVisibility.KeepHighlightDescriptions + var hideHighlightDescriptions = !options.DescriptionVisibility.HasFlag(ChangelogDescriptionVisibility.KeepHighlightDescriptions) + && hideEntryDescriptions; + var hideFeatureDescriptions = !options.DescriptionVisibility.HasFlag(ChangelogDescriptionVisibility.KeepFeatureDescriptions) && hideEntryDescriptions; var dropdownsEnabled = options.DropdownsEnabled; var typeFilter = options.TypeFilter; @@ -372,12 +385,18 @@ private static string GenerateMarkdown(BundleRenderModel model, ChangelogRenderO ); } - if (features.Count > 0 || enhancements.Count > 0) + if (features.Count > 0) + { + _ = sb.AppendLine(); + _ = sb.AppendLine(CultureInfo.InvariantCulture, $"### Features [{repo}-{titleSlug}-features]"); + RenderEntriesByArea(sb, features, repo, owner, subsections, hideLinks, hideFeatureDescriptions, publishBlocker); + } + + if (enhancements.Count > 0) { _ = sb.AppendLine(); - _ = sb.AppendLine(CultureInfo.InvariantCulture, $"### Features and enhancements [{repo}-{titleSlug}-features-enhancements]"); - var combined = features.Concat(enhancements).ToList(); - RenderEntriesByArea(sb, combined, repo, owner, subsections, hideLinks, hideEntryDescriptions, publishBlocker); + _ = sb.AppendLine(CultureInfo.InvariantCulture, $"### Enhancements [{repo}-{titleSlug}-enhancements]"); + RenderEntriesByArea(sb, enhancements, repo, owner, subsections, hideLinks, hideEntryDescriptions, publishBlocker); } if (bugFixes.Count > 0) diff --git a/src/services/Elastic.Changelog/Rendering/Asciidoc/ChangelogAsciidocRenderer.cs b/src/services/Elastic.Changelog/Rendering/Asciidoc/ChangelogAsciidocRenderer.cs index 93229ca8e4..e9b4cb67a7 100644 --- a/src/services/Elastic.Changelog/Rendering/Asciidoc/ChangelogAsciidocRenderer.cs +++ b/src/services/Elastic.Changelog/Rendering/Asciidoc/ChangelogAsciidocRenderer.cs @@ -83,12 +83,17 @@ public async Task RenderAsciidoc(ChangelogRenderContext context, Cancel ctx) _ = sb.AppendLine(); } - // Render features and enhancements - if (features.Count > 0 || enhancements.Count > 0) + if (features.Count > 0) { - RenderSectionHeader(sb, "features-enhancements", context.TitleSlug, "New features and enhancements"); - var combined = features.Concat(enhancements).ToList(); - entriesByAreaRenderer.Render(combined, context); + RenderSectionHeader(sb, "features", context.TitleSlug, "Features"); + entriesByAreaRenderer.Render(features, context); + _ = sb.AppendLine(); + } + + if (enhancements.Count > 0) + { + RenderSectionHeader(sb, "enhancements", context.TitleSlug, "Enhancements"); + entriesByAreaRenderer.Render(enhancements, context); _ = sb.AppendLine(); } diff --git a/src/services/Elastic.Changelog/Rendering/Markdown/ChangelogGfmRenderer.cs b/src/services/Elastic.Changelog/Rendering/Markdown/ChangelogGfmRenderer.cs index 3a3b981301..40a69fc3a8 100644 --- a/src/services/Elastic.Changelog/Rendering/Markdown/ChangelogGfmRenderer.cs +++ b/src/services/Elastic.Changelog/Rendering/Markdown/ChangelogGfmRenderer.cs @@ -71,16 +71,18 @@ bool AllEntriesHidden(IReadOnlyCollection entries) => _ = sb.AppendLine(); } - // Features and enhancements - if (features.Count > 0 || enhancements.Count > 0) + if (features.Count > 0 && !AllEntriesHidden(features)) { - var combined = features.Concat(enhancements).ToList(); - if (!AllEntriesHidden(combined)) - { - _ = sb.AppendLine("### Features and enhancements"); - RenderEntriesByArea(sb, combined, context); - _ = sb.AppendLine(); - } + _ = sb.AppendLine("### Features"); + RenderEntriesByArea(sb, features, context); + _ = sb.AppendLine(); + } + + if (enhancements.Count > 0 && !AllEntriesHidden(enhancements)) + { + _ = sb.AppendLine("### Enhancements"); + RenderEntriesByArea(sb, enhancements, context); + _ = sb.AppendLine(); } // Breaking changes diff --git a/src/services/Elastic.Changelog/Rendering/Markdown/IndexMarkdownRenderer.cs b/src/services/Elastic.Changelog/Rendering/Markdown/IndexMarkdownRenderer.cs index 203a343be8..dbc1ca5aa8 100644 --- a/src/services/Elastic.Changelog/Rendering/Markdown/IndexMarkdownRenderer.cs +++ b/src/services/Elastic.Changelog/Rendering/Markdown/IndexMarkdownRenderer.cs @@ -61,25 +61,33 @@ bool AllEntriesHidden(IReadOnlyCollection entries) => entries.Count > 0 && entries.All(entry => ChangelogRenderUtilities.ShouldHideEntry(entry, context.FeatureIdsToHide, context)); // Check if each category has visible entries - var hasVisibleFeatures = (features.Count > 0 || enhancements.Count > 0) - && !(AllEntriesHidden(features) && AllEntriesHidden(enhancements)); + var hasVisibleFeatures = features.Count > 0 && !AllEntriesHidden(features); + var hasVisibleEnhancements = enhancements.Count > 0 && !AllEntriesHidden(enhancements); var hasVisibleFixes = (security.Count > 0 || bugFixes.Count > 0) && !(AllEntriesHidden(security) && AllEntriesHidden(bugFixes)); var hasVisibleDocs = docs.Count > 0 && !AllEntriesHidden(docs); var hasVisibleRegressions = regressions.Count > 0 && !AllEntriesHidden(regressions); var hasVisibleOther = other.Count > 0 && !AllEntriesHidden(other); - var hasAnyVisibleEntries = hasVisibleFeatures || hasVisibleFixes || hasVisibleDocs || hasVisibleRegressions || hasVisibleOther; + var hasAnyVisibleEntries = hasVisibleFeatures + || hasVisibleEnhancements + || hasVisibleFixes + || hasVisibleDocs + || hasVisibleRegressions + || hasVisibleOther; if (hasAnyEntries) { - if (features.Count > 0 || enhancements.Count > 0) + if (features.Count > 0) { - var combined = features.Concat(enhancements).ToList(); - _ = sb.AppendLine( - InvariantCulture, - $"### Features and enhancements [{context.Repo}-{context.TitleSlug}-features-enhancements]" - ); - RenderEntriesByArea(sb, combined, context); + _ = sb.AppendLine(InvariantCulture, $"### Features [{context.Repo}-{context.TitleSlug}-features]"); + RenderEntriesByArea(sb, features, context); + } + + if (enhancements.Count > 0) + { + _ = sb.AppendLine(); + _ = sb.AppendLine(InvariantCulture, $"### Enhancements [{context.Repo}-{context.TitleSlug}-enhancements]"); + RenderEntriesByArea(sb, enhancements, context); } if (security.Count > 0 || bugFixes.Count > 0) diff --git a/tests/Elastic.Changelog.Tests/Changelogs/Render/GfmRenderTests.cs b/tests/Elastic.Changelog.Tests/Changelogs/Render/GfmRenderTests.cs index ec5414cdd6..2277fc113f 100644 --- a/tests/Elastic.Changelog.Tests/Changelogs/Render/GfmRenderTests.cs +++ b/tests/Elastic.Changelog.Tests/Changelogs/Render/GfmRenderTests.cs @@ -70,13 +70,13 @@ public async Task RenderChangelogs_WithGfmFileType_CreatesSingleGfmFile() var content = await FileSystem.File.ReadAllTextAsync(outputFile, TestContext.Current.CancellationToken); content.Should().Contain("## 9.2.0"); - content.Should().Contain("### Features and enhancements"); + content.Should().Contain("### Features"); content.Should().Contain("Test feature"); content.Should().Contain("[#100](https://github.com/elastic/elasticsearch/pull/100)"); // Should NOT contain anchor brackets in headings content.Should().NotContain("## 9.2.0 ["); - content.Should().NotContain("### Features and enhancements ["); + content.Should().NotContain("### Features ["); } [Fact] @@ -175,7 +175,7 @@ public async Task RenderChangelogs_WithGfmFileType_IncludesAllSectionTypes() var content = await FileSystem.File.ReadAllTextAsync(outputChangelogPath, TestContext.Current.CancellationToken); // Should include all section types in the proper order - content.Should().Contain("### Features and enhancements"); + content.Should().Contain("### Features"); content.Should().Contain("### Breaking changes"); content.Should().Contain("### Deprecations"); content.Should().Contain("### Bug fixes"); @@ -189,7 +189,7 @@ public async Task RenderChangelogs_WithGfmFileType_IncludesAllSectionTypes() content.Should().Contain("Known issue"); // Check section ordering (features should come before breaking changes) - var featuresIndex = content.IndexOf("### Features and enhancements", StringComparison.Ordinal); + var featuresIndex = content.IndexOf("### Features", StringComparison.Ordinal); var breakingIndex = content.IndexOf("### Breaking changes", StringComparison.Ordinal); var deprecationIndex = content.IndexOf("### Deprecations", StringComparison.Ordinal); var bugFixIndex = content.IndexOf("### Bug fixes", StringComparison.Ordinal); @@ -266,11 +266,11 @@ public async Task RenderChangelogs_WithGfmFileType_HandlesHighlights() // Should include highlights section first content.Should().Contain("### Highlights"); - content.Should().Contain("### Features and enhancements"); + content.Should().Contain("### Features"); // Highlights should come first var highlightsIndex = content.IndexOf("### Highlights", StringComparison.Ordinal); - var featuresIndex = content.IndexOf("### Features and enhancements", StringComparison.Ordinal); + var featuresIndex = content.IndexOf("### Features", StringComparison.Ordinal); highlightsIndex.Should().BeLessThan(featuresIndex); // Both features should be present diff --git a/tests/Elastic.Changelog.Tests/Changelogs/Render/OutputFormatTests.cs b/tests/Elastic.Changelog.Tests/Changelogs/Render/OutputFormatTests.cs index 7c1dccad01..4815e1c951 100644 --- a/tests/Elastic.Changelog.Tests/Changelogs/Render/OutputFormatTests.cs +++ b/tests/Elastic.Changelog.Tests/Changelogs/Render/OutputFormatTests.cs @@ -141,8 +141,8 @@ public async Task RenderChangelogs_WithAsciidocFileType_CreatesSingleAsciidocFil // Verify valid asciidoc format elements asciidocContent.Should().Contain("[[release-notes-", "should contain anchor"); asciidocContent.Should().Contain("== 9.2.0", "should contain section header"); - asciidocContent.Should().Contain("[[features-enhancements-", "should contain features section anchor"); - asciidocContent.Should().Contain("=== New features and enhancements", "should contain features section header"); + asciidocContent.Should().Contain("[[features-", "should contain features section anchor"); + asciidocContent.Should().Contain("=== Features", "should contain features section header"); asciidocContent.Should().Contain("* Test feature", "should contain changelog entry"); asciidocContent.Should().Contain("This is a test feature", "should contain description"); @@ -247,8 +247,8 @@ public async Task RenderChangelogs_WithAsciidocFileType_ValidatesAsciidocFormat( asciidocContent.Should().Contain("[float]", "should contain float attribute"); asciidocContent.Should().Contain("=== Bug fixes", "should contain bug fixes header"); - asciidocContent.Should().Contain("[[features-enhancements-9.2.0]]", "should contain features anchor"); - asciidocContent.Should().Contain("=== New features and enhancements", "should contain features header"); + asciidocContent.Should().Contain("[[features-9.2.0]]", "should contain features anchor"); + asciidocContent.Should().Contain("=== Features", "should contain features header"); asciidocContent.Should().Contain("[[breaking-changes-9.2.0]]", "should contain breaking changes anchor"); asciidocContent.Should().Contain("=== Breaking changes", "should contain breaking changes header"); diff --git a/tests/Elastic.Markdown.Tests/Directives/ChangelogBasicTests.cs b/tests/Elastic.Markdown.Tests/Directives/ChangelogBasicTests.cs index 47888bf488..96b769d838 100644 --- a/tests/Elastic.Markdown.Tests/Directives/ChangelogBasicTests.cs +++ b/tests/Elastic.Markdown.Tests/Directives/ChangelogBasicTests.cs @@ -77,7 +77,7 @@ public void SetsCorrectBundlesFolderPath() => public void RendersMarkdownContent() { Html.Should().Contain("9.3.0"); - Html.Should().Contain("Features and enhancements"); + Html.Should().Contain("Features"); Html.Should().Contain("Add new feature"); Html.Should().Contain("Fixes"); Html.Should().Contain("Fix important bug"); @@ -490,7 +490,7 @@ protected override IReleaseNotesResolver GetReleaseNotesResolver() => public void RendersCdnBundleBody() { Html.Should().Contain("9.4.0"); - Html.Should().Contain("Features and enhancements"); + Html.Should().Contain("Enhancements"); Html.Should().Contain("Faster vector search on the CDN"); } } @@ -1002,7 +1002,7 @@ public ChangelogSectionOrderTests(ITestOutputHelper output) : base( public void BreakingChangesAppearsFirst() { var breakingIdx = Html.IndexOf("Breaking changes", StringComparison.Ordinal); - var featuresIdx = Html.IndexOf("Features and enhancements", StringComparison.Ordinal); + var featuresIdx = Html.IndexOf("Features", StringComparison.Ordinal); var fixesIdx = Html.IndexOf(">Fixes<", StringComparison.Ordinal); breakingIdx.Should().BeLessThan(featuresIdx, "Breaking changes should appear before Features"); @@ -1013,7 +1013,7 @@ public void BreakingChangesAppearsFirst() public void SecurityAppearsBeforeFeatures() { var securityIdx = Html.IndexOf(">Security<", StringComparison.Ordinal); - var featuresIdx = Html.IndexOf("Features and enhancements", StringComparison.Ordinal); + var featuresIdx = Html.IndexOf("Features", StringComparison.Ordinal); securityIdx.Should().BeLessThan(featuresIdx, "Security should appear before Features"); } @@ -1022,7 +1022,7 @@ public void SecurityAppearsBeforeFeatures() public void KnownIssuesAppearsBeforeFeatures() { var knownIssuesIdx = Html.IndexOf("Known issues", StringComparison.Ordinal); - var featuresIdx = Html.IndexOf("Features and enhancements", StringComparison.Ordinal); + var featuresIdx = Html.IndexOf("Features", StringComparison.Ordinal); knownIssuesIdx.Should().BeLessThan(featuresIdx, "Known issues should appear before Features"); } @@ -1031,7 +1031,7 @@ public void KnownIssuesAppearsBeforeFeatures() public void DeprecationsAppearsBeforeFeatures() { var deprecationsIdx = Html.IndexOf("Deprecations", StringComparison.Ordinal); - var featuresIdx = Html.IndexOf("Features and enhancements", StringComparison.Ordinal); + var featuresIdx = Html.IndexOf("Features", StringComparison.Ordinal); deprecationsIdx.Should().BeLessThan(featuresIdx, "Deprecations should appear before Features"); } diff --git a/tests/Elastic.Markdown.Tests/Directives/ChangelogConfigTests.cs b/tests/Elastic.Markdown.Tests/Directives/ChangelogConfigTests.cs index 0f15b2a764..cc5a8befdf 100644 --- a/tests/Elastic.Markdown.Tests/Directives/ChangelogConfigTests.cs +++ b/tests/Elastic.Markdown.Tests/Directives/ChangelogConfigTests.cs @@ -89,7 +89,7 @@ public void RendersAllEntries_NoFiltering() } [Fact] - public void RendersFeaturesSection() => Html.Should().Contain("Features and enhancements"); + public void RendersFeaturesSection() => Html.Should().Contain("Features"); [Fact] public void RendersDeprecationsSection() => Html.Should().Contain("Deprecations"); diff --git a/tests/Elastic.Markdown.Tests/Directives/ChangelogDescriptionVisibilityTests.cs b/tests/Elastic.Markdown.Tests/Directives/ChangelogDescriptionVisibilityTests.cs index 637f624d0d..35677b48ee 100644 --- a/tests/Elastic.Markdown.Tests/Directives/ChangelogDescriptionVisibilityTests.cs +++ b/tests/Elastic.Markdown.Tests/Directives/ChangelogDescriptionVisibilityTests.cs @@ -38,18 +38,37 @@ public void KeepDescriptions_AlwaysReturnsFalse() } [Fact] - public void KeepHighlightDescriptions_AlwaysReturnsTrue() + public void KeepFeatureDescriptions_AlwaysReturnsTrue() { - // Default path hides descriptions; Highlights section overrides separately in the renderer. var result = ChangelogInlineRenderer.ShouldHideEntryDescriptionsForRepo( "kibana", [], - ChangelogDescriptionVisibility.KeepHighlightDescriptions + ChangelogDescriptionVisibility.KeepFeatureDescriptions ); result.Should().BeTrue(); } + [Fact] + public void CombinedFeatureAndHighlightOverlays_AlwaysReturnsTrue() + { + var visibility = ChangelogDescriptionVisibility.KeepFeatureDescriptions | ChangelogDescriptionVisibility.KeepHighlightDescriptions; + + var result = ChangelogInlineRenderer.ShouldHideEntryDescriptionsForRepo("kibana", [], visibility); + + result.Should().BeTrue(); + } + + [Fact] + public void AutoPlusFeatureOverlay_WithPublicRepo_HidesDefaultBodies() + { + var visibility = ChangelogDescriptionVisibility.Auto | ChangelogDescriptionVisibility.KeepFeatureDescriptions; + + var result = ChangelogInlineRenderer.ShouldHideEntryDescriptionsForRepo("kibana", [], visibility); + + result.Should().BeTrue(); + } + [Fact] public void Auto_WithEmptyPrivateRepos_HidesBodies() { @@ -313,3 +332,234 @@ protected override void AddToFileSystem(MockFileSystem fileSystem) => [Fact] public void AutoTreatsFullyPublic_AsHideBody() => Html.Should().NotContain("BODY_INVALID_VISIBILITY"); } + +public class ChangelogKeepFeatureDescriptionsTests(ITestOutputHelper output) : DirectiveTest( + output, + """ + :::{changelog} + :description-visibility: keep-feature-descriptions + ::: + """ +) +{ + protected override void AddToFileSystem(MockFileSystem fileSystem) => + fileSystem.AddFile( + "docs/changelog/bundles/9.3.0.yaml", + new MockFileData( + """ + products: + - product: elasticsearch + target: 9.3.0 + entries: + - title: Feature with body + type: feature + products: + - product: elasticsearch + target: 9.3.0 + description: FEATURE_ONLY_BODY + - title: Enhancement with body + type: enhancement + products: + - product: elasticsearch + target: 9.3.0 + description: ENHANCEMENT_HIDDEN_BODY + """ + ) + ); + + [Fact] + public void ParsesKeepFeatureDescriptions() => + Block!.DescriptionVisibility.Should().Be(ChangelogDescriptionVisibility.KeepFeatureDescriptions); + + [Fact] + public void ShowsFeatureBodies() => Html.Should().Contain("FEATURE_ONLY_BODY"); + + [Fact] + public void HidesEnhancementBodies() => Html.Should().NotContain("ENHANCEMENT_HIDDEN_BODY"); +} + +public class ChangelogCombinedFeatureAndHighlightDescriptionsTests(ITestOutputHelper output) : DirectiveTest( + output, + """ + :::{changelog} + :highlights: + :description-visibility: keep-feature-descriptions, keep-highlight-descriptions + ::: + """ +) +{ + protected override void AddToFileSystem(MockFileSystem fileSystem) => + fileSystem.AddFile( + "docs/changelog/bundles/9.3.0.yaml", + new MockFileData( + """ + products: + - product: elasticsearch + target: 9.3.0 + entries: + - title: Highlighted feature + type: feature + highlight: true + products: + - product: elasticsearch + target: 9.3.0 + description: HIGHLIGHT_SECTION_BODY + - title: Regular feature + type: feature + products: + - product: elasticsearch + target: 9.3.0 + description: FEATURE_SECTION_BODY + - title: Enhancement with body + type: enhancement + products: + - product: elasticsearch + target: 9.3.0 + description: ENHANCEMENT_HIDDEN_BODY + """ + ) + ); + + [Fact] + public void ParsesBothOverlays() + { + Block!.DescriptionVisibility.HasFlag(ChangelogDescriptionVisibility.KeepFeatureDescriptions).Should().BeTrue(); + Block!.DescriptionVisibility.HasFlag(ChangelogDescriptionVisibility.KeepHighlightDescriptions).Should().BeTrue(); + } + + [Fact] + public void ShowsHighlightBodies() => Html.Should().Contain("HIGHLIGHT_SECTION_BODY"); + + [Fact] + public void ShowsFeatureBodies() => Html.Should().Contain("FEATURE_SECTION_BODY"); + + [Fact] + public void HidesEnhancementBodies() => Html.Should().NotContain("ENHANCEMENT_HIDDEN_BODY"); +} + +public class ChangelogAutoPlusFeatureDescriptionsTests(ITestOutputHelper output) : DirectiveTest( + output, + """ + :::{changelog} + :description-visibility: auto, keep-feature-descriptions + ::: + """ +) +{ + protected override void AddToFileSystem(MockFileSystem fileSystem) => + fileSystem.AddFile( + "docs/changelog/bundles/9.3.0.yaml", + new MockFileData( + """ + products: + - product: elasticsearch + target: 9.3.0 + entries: + - title: Feature with body + type: feature + products: + - product: elasticsearch + target: 9.3.0 + description: FEATURE_AUTO_OVERLAY_BODY + - title: Enhancement with body + type: enhancement + products: + - product: elasticsearch + target: 9.3.0 + description: ENHANCEMENT_AUTO_BODY + """ + ) + ); + + [Fact] + public void ParsesAutoPlusFeatureOverlay() + { + Block!.DescriptionVisibility.HasFlag(ChangelogDescriptionVisibility.Auto).Should().BeTrue(); + Block!.DescriptionVisibility.HasFlag(ChangelogDescriptionVisibility.KeepFeatureDescriptions).Should().BeTrue(); + } + + [Fact] + public void ShowsFeatureBodiesOnPublicRepos() => Html.Should().Contain("FEATURE_AUTO_OVERLAY_BODY"); + + [Fact] + public void HidesEnhancementBodiesOnPublicRepos() => Html.Should().NotContain("ENHANCEMENT_AUTO_BODY"); +} + +public class ChangelogDuplicateBaseDescriptionVisibilityTests(ITestOutputHelper output) : DirectiveTest( + output, + """ + :::{changelog} + :description-visibility: keep-descriptions, hide-descriptions + ::: + """ +) +{ + protected override void AddToFileSystem(MockFileSystem fileSystem) => + fileSystem.AddFile( + "docs/changelog/bundles/9.3.0.yaml", + new MockFileData( + """ + products: + - product: elasticsearch + target: 9.3.0 + entries: + - title: Feature keep first + type: feature + products: + - product: elasticsearch + target: 9.3.0 + description: BODY_FIRST_BASE_WINS + """ + ) + ); + + [Fact] + public void KeepsFirstBaseToken() => Block!.DescriptionVisibility.Should().Be(ChangelogDescriptionVisibility.KeepDescriptions); + + [Fact] + public void EmitsWarning() => Collector.Warnings.Should().BeGreaterThan(0); + + [Fact] + public void RendersBodiesUsingFirstBase() => Html.Should().Contain("BODY_FIRST_BASE_WINS"); +} + +public class ChangelogHideDescriptionsPlusFeatureOverlayTests(ITestOutputHelper output) : DirectiveTest( + output, + """ + :::{changelog} + :description-visibility: hide-descriptions, keep-feature-descriptions + ::: + """ +) +{ + protected override void AddToFileSystem(MockFileSystem fileSystem) => + fileSystem.AddFile( + "docs/changelog/bundles/9.3.0.yaml", + new MockFileData( + """ + products: + - product: elasticsearch + target: 9.3.0 + entries: + - title: Feature with body + type: feature + products: + - product: elasticsearch + target: 9.3.0 + description: FEATURE_OVERLAY_WINS + - title: Enhancement with body + type: enhancement + products: + - product: elasticsearch + target: 9.3.0 + description: ENHANCEMENT_STILL_HIDDEN + """ + ) + ); + + [Fact] + public void ShowsFeatureBodiesDespiteHideBase() => Html.Should().Contain("FEATURE_OVERLAY_WINS"); + + [Fact] + public void HidesEnhancementBodies() => Html.Should().NotContain("ENHANCEMENT_STILL_HIDDEN"); +} diff --git a/tests/Elastic.Markdown.Tests/Directives/ChangelogFeatureEnhancementSectionTests.cs b/tests/Elastic.Markdown.Tests/Directives/ChangelogFeatureEnhancementSectionTests.cs new file mode 100644 index 0000000000..032506289e --- /dev/null +++ b/tests/Elastic.Markdown.Tests/Directives/ChangelogFeatureEnhancementSectionTests.cs @@ -0,0 +1,145 @@ +// 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.Markdown.Myst.Directives.Changelog; + +namespace Elastic.Markdown.Tests.Directives; + +public class ChangelogFeatureAndEnhancementSectionsTests(ITestOutputHelper output) : DirectiveTest( + output, + """ + :::{changelog} + ::: + """ +) +{ + protected override void AddToFileSystem(MockFileSystem fileSystem) => + fileSystem.AddFile( + "docs/changelog/bundles/9.3.0.yaml", + new MockFileData( + """ + products: + - product: elasticsearch + target: 9.3.0 + entries: + - title: Brand new capability + type: feature + products: + - product: elasticsearch + target: 9.3.0 + - title: Improved existing capability + type: enhancement + products: + - product: elasticsearch + target: 9.3.0 + - title: Bug fix + type: bug-fix + products: + - product: elasticsearch + target: 9.3.0 + """ + ) + ); + + [Fact] + public void RendersSeparateFeaturesHeading() => Html.Should().Contain(">Features<"); + + [Fact] + public void RendersSeparateEnhancementsHeading() => Html.Should().Contain(">Enhancements<"); + + [Fact] + public void OmitsCombinedHeading() => Html.Should().NotContain("Features and enhancements"); + + [Fact] + public void FeaturesAppearBeforeEnhancements() + { + var featuresIdx = Html.IndexOf(">Features<", StringComparison.Ordinal); + var enhancementsIdx = Html.IndexOf(">Enhancements<", StringComparison.Ordinal); + featuresIdx.Should().BeLessThan(enhancementsIdx); + } + + [Fact] + public void TocIncludesBothSections() + { + var toc = Block!.GeneratedTableOfContent.ToList(); + toc.Should().Contain(t => t.Heading == "Features" && t.Slug.EndsWith("-features")); + toc.Should().Contain(t => t.Heading == "Enhancements" && t.Slug.EndsWith("-enhancements")); + } +} + +public class ChangelogEnhancementsOnlyOmitsFeaturesSectionTests(ITestOutputHelper output) : DirectiveTest( + output, + """ + :::{changelog} + ::: + """ +) +{ + protected override void AddToFileSystem(MockFileSystem fileSystem) => + fileSystem.AddFile( + "docs/changelog/bundles/9.3.6.yaml", + new MockFileData( + """ + products: + - product: elasticsearch + target: 9.3.6 + entries: + - title: Faster existing query + type: enhancement + products: + - product: elasticsearch + target: 9.3.6 + """ + ) + ); + + [Fact] + public void RendersEnhancements() => Html.Should().Contain(">Enhancements<"); + + [Fact] + public void OmitsEmptyFeaturesSection() => Html.Should().NotContain(">Features<"); + + [Fact] + public void TocOmitsFeatures() + { + var toc = Block!.GeneratedTableOfContent.ToList(); + toc.Should().NotContain(t => t.Heading == "Features"); + toc.Should().Contain(t => t.Heading == "Enhancements"); + } +} + +public class ChangelogFeaturesOnlyOmitsEnhancementsSectionTests(ITestOutputHelper output) : DirectiveTest( + output, + """ + :::{changelog} + ::: + """ +) +{ + protected override void AddToFileSystem(MockFileSystem fileSystem) => + fileSystem.AddFile( + "docs/changelog/bundles/9.4.0.yaml", + new MockFileData( + """ + products: + - product: elasticsearch + target: 9.4.0 + entries: + - title: Entirely new API + type: feature + products: + - product: elasticsearch + target: 9.4.0 + """ + ) + ); + + [Fact] + public void RendersFeatures() => Html.Should().Contain(">Features<"); + + [Fact] + public void OmitsEmptyEnhancementsSection() => Html.Should().NotContain(">Enhancements<"); +} diff --git a/tests/Elastic.Markdown.Tests/Directives/ChangelogHideLinksTests.cs b/tests/Elastic.Markdown.Tests/Directives/ChangelogHideLinksTests.cs index 39f7dd567d..50a9929d50 100644 --- a/tests/Elastic.Markdown.Tests/Directives/ChangelogHideLinksTests.cs +++ b/tests/Elastic.Markdown.Tests/Directives/ChangelogHideLinksTests.cs @@ -453,7 +453,7 @@ public void MergedBundle_TocSlug_MatchesHeadingId() { var section = Block!.GeneratedTableOfContent.Single(t => t.Level == 3); - section.Slug.Should().Be("elasticsearchkibana-2025-08-05-features-enhancements"); + section.Slug.Should().Be("elasticsearchkibana-2025-08-05-features"); Html.Should().Contain($"id=\"{section.Slug}\""); } diff --git a/tests/Elastic.Markdown.Tests/Directives/ChangelogHighlightsOptionTests.cs b/tests/Elastic.Markdown.Tests/Directives/ChangelogHighlightsOptionTests.cs index c9eddb2289..ce62607bb5 100644 --- a/tests/Elastic.Markdown.Tests/Directives/ChangelogHighlightsOptionTests.cs +++ b/tests/Elastic.Markdown.Tests/Directives/ChangelogHighlightsOptionTests.cs @@ -78,7 +78,7 @@ public ChangelogHighlightsOptionDefaultOffTests(ITestOutputHelper output) : base [Fact] public void StillRendersHighlightedEntryUnderTypeSection() { - Html.Should().Contain("Features and enhancements"); + Html.Should().Contain("Features"); Html.Should().Contain("Highlighted feature"); Html.Should().Contain("Regular feature"); Html.Should().Contain("Fixes"); @@ -118,7 +118,7 @@ public ChangelogHighlightsOptionEnabledTests(ITestOutputHelper output) : base( [Fact] public void DuplicatesHighlightedEntryInTypeSection() { - Html.Should().Contain("Features and enhancements"); + Html.Should().Contain("Features"); Html.Should().Contain("Highlighted feature"); Html.Should().Contain("Regular feature"); } @@ -157,7 +157,7 @@ public void RendersHighlightsAndSeparatedTypes() { Html.Should().Contain("Highlights"); Html.Should().Contain("Breaking changes"); - Html.Should().Contain("Features and enhancements"); + Html.Should().Contain("Features"); } } @@ -218,7 +218,7 @@ public void EmitsWarningPointingToHighlightsOption() => [Fact] public void RendersDefaultTypeSectionsNotHighlightsOnly() { - Html.Should().Contain("Features and enhancements"); + Html.Should().Contain("Features"); Html.Should().Contain("Highlighted feature"); Html.Should().NotContain("id=\"elasticsearch-9.3.0-highlights\""); } @@ -300,7 +300,7 @@ public void ParsesKeepHighlightDescriptions() => [Fact] public void StillRendersTitlesInTypeSections() { - Html.Should().Contain("Features and enhancements"); + Html.Should().Contain("Features"); Html.Should().Contain("Highlighted feature"); Html.Should().Contain("Regular feature"); } diff --git a/tests/Elastic.Markdown.Tests/Directives/ChangelogTocFilteringTests.cs b/tests/Elastic.Markdown.Tests/Directives/ChangelogTocFilteringTests.cs index 61fada3446..72e09876bc 100644 --- a/tests/Elastic.Markdown.Tests/Directives/ChangelogTocFilteringTests.cs +++ b/tests/Elastic.Markdown.Tests/Directives/ChangelogTocFilteringTests.cs @@ -94,7 +94,7 @@ public void TocIncludesOtherSection() public void TocIncludesFeaturesSection() { var tocItems = Block!.GeneratedTableOfContent.ToList(); - tocItems.Should().Contain(t => t.Heading == "Features and enhancements"); + tocItems.Should().Contain(t => t.Heading == "Features"); } [Fact] @@ -123,7 +123,7 @@ public void HtmlContainsAllSections() { Html.Should().Contain("Documentation"); Html.Should().Contain("Other changes"); - Html.Should().Contain("Features and enhancements"); + Html.Should().Contain("Features"); } } @@ -193,7 +193,7 @@ public void TocExcludesHiddenOtherSection() public void TocRetainsVisibleFeaturesSection() { var tocItems = Block!.GeneratedTableOfContent.ToList(); - tocItems.Should().Contain(t => t.Heading == "Features and enhancements"); + tocItems.Should().Contain(t => t.Heading == "Features"); } [Fact] @@ -207,7 +207,7 @@ public void AnchorsExcludeHiddenOtherSection() public void AnchorsRetainVisibleFeaturesSection() { var anchors = Block!.GeneratedAnchors.ToList(); - anchors.Should().Contain(a => a.Contains("features-enhancements")); + anchors.Should().Contain(a => a.Contains("features")); } [Fact] @@ -215,7 +215,7 @@ public void HtmlMatchesTocFiltering() { Html.Should().NotContain("Other changes"); Html.Should().NotContain("Hidden other change"); - Html.Should().Contain("Features and enhancements"); + Html.Should().Contain("Features"); Html.Should().Contain("Visible feature"); } } @@ -371,7 +371,7 @@ public void TocExcludesHideFeatureFilteredSection() public void TocRetainsUnfilteredSection() { var tocItems = Block!.GeneratedTableOfContent.ToList(); - tocItems.Should().Contain(t => t.Heading == "Features and enhancements"); + tocItems.Should().Contain(t => t.Heading == "Features"); } [Fact] @@ -385,7 +385,7 @@ public void AnchorsExcludeOnlyHideFeatureFilteredSection() [Fact] public void HtmlMatchesTocAndAnchors() { - Html.Should().Contain("Features and enhancements"); + Html.Should().Contain("Features"); Html.Should().Contain("Visible feature"); Html.Should().Contain("Documentation"); Html.Should().Contain("Docs entry blocked by type"); @@ -483,7 +483,7 @@ public void TocIncludesOtherSection() public void TocIncludesFeaturesSection() { var tocItems = Block!.GeneratedTableOfContent.ToList(); - tocItems.Should().Contain(t => t.Heading == "Features and enhancements"); + tocItems.Should().Contain(t => t.Heading == "Features"); } [Fact] @@ -492,7 +492,7 @@ public void AnchorsIncludeAllSections() var anchors = Block!.GeneratedAnchors.ToList(); anchors.Should().Contain(a => a.Contains("-docs")); anchors.Should().Contain(a => a.EndsWith("-other")); - anchors.Should().Contain(a => a.Contains("features-enhancements")); + anchors.Should().Contain(a => a.Contains("features")); } } @@ -658,7 +658,7 @@ public void BothVersionsHaveDocumentationInToc() public void FirstVersionRetainsFeaturesInToc() { var tocItems = Block!.GeneratedTableOfContent.ToList(); - tocItems.Should().Contain(t => t.Heading == "Features and enhancements"); + tocItems.Should().Contain(t => t.Heading == "Features"); } [Fact] diff --git a/tests/Elastic.Markdown.Tests/Directives/ChangelogTypeFilterTests.cs b/tests/Elastic.Markdown.Tests/Directives/ChangelogTypeFilterTests.cs index 25ed1f3e81..045d798425 100644 --- a/tests/Elastic.Markdown.Tests/Directives/ChangelogTypeFilterTests.cs +++ b/tests/Elastic.Markdown.Tests/Directives/ChangelogTypeFilterTests.cs @@ -89,7 +89,7 @@ public ChangelogTypeFilterDefaultTests(ITestOutputHelper output) : base( [Fact] public void DefaultBehaviorShowsFeatures() { - Html.Should().Contain("Features and enhancements"); + Html.Should().Contain("Features"); Html.Should().Contain("New feature"); } @@ -199,7 +199,7 @@ public ChangelogTypeFilterAllTests(ITestOutputHelper output) : base( [Fact] public void ShowsAllEntryTypes() { - Html.Should().Contain("Features and enhancements"); + Html.Should().Contain("Features"); Html.Should().Contain("New feature"); Html.Should().Contain(">Fixes<"); Html.Should().Contain("Bug fix"); @@ -303,7 +303,7 @@ public void ShowsBreakingChanges() [Fact] public void ExcludesOtherTypes() { - Html.Should().NotContain("Features and enhancements"); + Html.Should().NotContain("Features"); Html.Should().NotContain("New feature"); Html.Should().NotContain("Known issues"); Html.Should().NotContain("Known issue"); @@ -379,7 +379,7 @@ public void ShowsDeprecations() [Fact] public void ExcludesOtherTypes() { - Html.Should().NotContain("Features and enhancements"); + Html.Should().NotContain("Features"); Html.Should().NotContain("New feature"); } } @@ -453,7 +453,7 @@ public void ShowsKnownIssues() [Fact] public void ExcludesOtherTypes() { - Html.Should().NotContain("Features and enhancements"); + Html.Should().NotContain("Features"); Html.Should().NotContain("New feature"); } } @@ -511,7 +511,7 @@ public ChangelogTypeFilterInvalidTests(ITestOutputHelper output) : base( [Fact] public void DefaultBehaviorIsApplied() { - Html.Should().Contain("Features and enhancements"); + Html.Should().Contain("Features"); Html.Should().Contain("New feature"); Html.Should().NotContain("Breaking changes"); } @@ -567,7 +567,7 @@ public ChangelogTypeFilterCaseInsensitiveTests(ITestOutputHelper output) : base( [Fact] public void ShowsAllTypes() { - Html.Should().Contain("Features and enhancements"); + Html.Should().Contain("Features"); Html.Should().Contain("Breaking changes"); } } @@ -638,7 +638,7 @@ public void TypeFilterAndSubsectionsBothWork() [Fact] public void ShowsAllTypesWithSubsections() { - Html.Should().Contain("Features and enhancements"); + Html.Should().Contain("Features"); Html.Should().Contain("Search feature"); Html.Should().Contain("Indexing feature"); Html.Should().Contain("Breaking changes"); @@ -695,7 +695,7 @@ public void GeneratedAnchorsRespectTypeFilter() var anchors = Block!.GeneratedAnchors.ToList(); anchors.Should().NotContain(a => a.Contains("breaking-changes")); - anchors.Should().NotContain(a => a.Contains("features-enhancements")); + anchors.Should().NotContain(a => a.Contains("features")); } } @@ -750,7 +750,7 @@ public void TableOfContentsRespectTypeFilter() tocItems.Should().NotContain(t => t.Heading == "Deprecations"); tocItems.Should().Contain(t => t.Heading == "9.3.0" && t.Level == 2); - tocItems.Should().NotContain(t => t.Heading == "Features and enhancements"); + tocItems.Should().NotContain(t => t.Heading == "Features"); } } From e35537d9b9f2eec8304a103e34ff3b9f7a097b9d Mon Sep 17 00:00:00 2001 From: lcawl Date: Wed, 9 Sep 2026 16:28:40 -0500 Subject: [PATCH 2/2] Fix subsections --- docs/syntax/changelog.md | 33 ++- .../Changelog/ChangelogInlineRenderer.cs | 40 ++- .../Directives/ChangelogDropdownsTests.cs | 265 +++++++++++++++++- 3 files changed, 313 insertions(+), 25 deletions(-) diff --git a/docs/syntax/changelog.md b/docs/syntax/changelog.md index fbe27415d0..f10b8c5af8 100644 --- a/docs/syntax/changelog.md +++ b/docs/syntax/changelog.md @@ -153,14 +153,16 @@ The example shows prose in Features and Highlights, and hides it in Enhancements #### `:dropdowns:` [dropdowns] -Controls how the "separated" entry types (`breaking-change`, `deprecation`, `known-issue`, and entries flagged `highlight: true`) are rendered. This option only affects these types; features, enhancements, security, bug fixes, documentation, regressions, and other changes are always rendered as flat bulleted lists. +Controls how breaking changes, deprecations, features, highlights, and known issues are rendered. Enhancements, security, bug fixes, documentation, regressions, and other changes stay flat bulleted lists. | Mode | Behavior | |------|----------| | (omitted, default) | Flattened: each entry renders as a bullet with its title, links, and (when present) `Impact:` / `Action:` lines as indented continuation. | | `:dropdowns:` | Dropdowns: each entry renders as an expandable `{dropdown}` with the title as the summary and description, links, `**Impact**`, and `**Action**` inside. | -Use dropdowns when breaking-change and deprecation entries have long `description`, `impact`, or `action` prose that benefits from being collapsed by default. Use the flattened default for compact release notes where the list itself is the primary content. +Use dropdowns when breaking changes, deprecations, features, or highlights have long `description`, `impact`, or `action` prose that benefits from being collapsed by default. +Use the flattened default for compact release notes where the list itself is the primary content. +`:dropdowns:` does not group entries by area; use [`:subsections:`](#subsections) for that. Entry titles may contain inline markdown markers from changelog YAML (for example, `` `setting.name` ``). Dropdown titles are plain text; see [Plain-text titles](/syntax/dropdowns.md#plain-text-titles). @@ -190,11 +192,12 @@ Use this option for semver or agent releases where an explicit release date adds This is **render-time** control only. To include or omit `release-date` in bundle YAML at build time, use `bundle.release_dates` in `changelog.yml` or the `--release-date` / `--no-release-date` flags on [`changelog bundle`](/cli/changelog/bundle.md) (option-based mode). The `changelog render` command does not provide an equivalent flag; it always renders release dates when present in the bundle. -#### `:subsections:` +#### `:subsections:` [subsections] When enabled, entries are grouped by "area" within each section. By default, entries are listed without area grouping. If a changelog has multiple area values, only the first one is used. +This option is independent of [`:dropdowns:`](#dropdowns). #### `:config:` @@ -421,27 +424,33 @@ Each changelog entry may have its own `description` field in YAML (shown as body | Section | Entry type | Rendering | |---------|------------|-----------| -| Features | `feature` | Grouped by area | -| Enhancements | `enhancement` | Grouped by area | -| Fixes | `bug-fix`, `security` | Grouped by area | -| Documentation | `docs` | Grouped by area | -| Regressions | `regression` | Grouped by area | -| Other changes | `other` | Grouped by area | +| Features | `feature` | Flattened bullets by default; expandable dropdowns with [`:dropdowns:`](#dropdowns) | +| Enhancements | `enhancement` | Flattened bullets | +| Fixes | `bug-fix` | Flattened bullets | +| Security | `security` | Flattened bullets | +| Documentation | `docs` | Flattened bullets | +| Regressions | `regression` | Flattened bullets | +| Other changes | `other` | Flattened bullets | | Breaking changes | `breaking-change` | Flattened bullets by default; expandable dropdowns with [`:dropdowns:`](#dropdowns) | | Highlights | Entries with `highlight: true` | Dedicated section only when [`:highlights:`](#highlights) is set; flattened bullets by default; expandable dropdowns with [`:dropdowns:`](#dropdowns) | | Deprecations | `deprecation` | Flattened bullets by default; expandable dropdowns with [`:dropdowns:`](#dropdowns) | | Known issues | `known-issue` | Flattened bullets by default; expandable dropdowns with [`:dropdowns:`](#dropdowns) | -**Note about highlights:** +Use [`:subsections:`](#subsections) to group entries by area within a section. Area grouping is off by default and is independent of `:dropdowns:`. + +:::{admonition} Highlights - The highlights section appears only when [`:highlights:`](#highlights) is set and at least one entry has `highlight: true` -- When the section is shown, highlighted entries appear in **both** the highlights section and their original type section (for example, both the "highlights" and "features" sections) +- When the section is shown, highlighted entries appear in both the highlights section and their original type section (for example, both the "highlights" and "features" sections) - When `:highlights:` is omitted, flagged entries still appear under their type sections (inline only) - You can combine `:highlights:` with the default type filter (no `:type:`) to show highlights alongside features and fixes without including breaking changes, deprecations, or known issues +::: + Sections with no entries of that type are omitted from the output. Releases with no entries after the `:type:` filter are omitted entirely, except on general release notes (`:type: all` or default) when the bundle has a `description`. -Section heading anchors use `-features` and `-enhancements` suffixes. Pages that previously linked to `-features-enhancements` need those links updated. +Section heading anchors use `-features` and `-enhancements` suffixes. +Pages that previously linked to `-features-enhancements` need those links updated. ## Error behavior for invalid entries [changelog-missing-files] diff --git a/src/Elastic.Markdown/Myst/Directives/Changelog/ChangelogInlineRenderer.cs b/src/Elastic.Markdown/Myst/Directives/Changelog/ChangelogInlineRenderer.cs index 49f75230d9..a252e37093 100644 --- a/src/Elastic.Markdown/Myst/Directives/Changelog/ChangelogInlineRenderer.cs +++ b/src/Elastic.Markdown/Myst/Directives/Changelog/ChangelogInlineRenderer.cs @@ -319,6 +319,7 @@ private static string GenerateMarkdown(BundleRenderModel model, ChangelogRenderO groupBySubtype: true, hideLinks, hideEntryDescriptions, + subsections, publishBlocker ); else @@ -338,6 +339,7 @@ private static string GenerateMarkdown(BundleRenderModel model, ChangelogRenderO groupBySubtype: false, hideLinks, hideHighlightDescriptions, + subsections, publishBlocker ); else @@ -389,7 +391,20 @@ private static string GenerateMarkdown(BundleRenderModel model, ChangelogRenderO { _ = sb.AppendLine(); _ = sb.AppendLine(CultureInfo.InvariantCulture, $"### Features [{repo}-{titleSlug}-features]"); - RenderEntriesByArea(sb, features, repo, owner, subsections, hideLinks, hideFeatureDescriptions, publishBlocker); + if (dropdownsEnabled) + RenderDetailedEntries( + sb, + features, + repo, + owner, + groupBySubtype: false, + hideLinks, + hideFeatureDescriptions, + subsections, + publishBlocker + ); + else + RenderEntriesByArea(sb, features, repo, owner, subsections, hideLinks, hideFeatureDescriptions, publishBlocker); } if (enhancements.Count > 0) @@ -528,12 +543,11 @@ private static void RenderDetailedEntries( bool groupBySubtype, bool hideLinks, bool hideEntryDescriptions, + bool subsections, PublishBlocker? publishBlocker ) { - var grouped = groupBySubtype - ? entries.GroupBy(e => e.Subtype?.ToStringFast(true) ?? string.Empty).OrderBy(g => g.Key).ToList() - : entries.GroupBy(e => publishBlocker.GetPreferredArea(e)).OrderBy(g => g.Key).ToList(); + var grouped = GroupDetailedEntries(entries, groupBySubtype, subsections, publishBlocker); foreach (var group in grouped) { @@ -551,6 +565,22 @@ private static void RenderDetailedEntries( } } + private static List> GroupDetailedEntries( + List entries, + bool groupBySubtype, + bool subsections, + PublishBlocker? publishBlocker + ) + { + if (groupBySubtype) + return entries.GroupBy(e => e.Subtype?.ToStringFast(true) ?? string.Empty).OrderBy(g => g.Key).ToList(); + + if (!subsections) + return entries.GroupBy(_ => string.Empty).ToList(); + + return entries.GroupBy(e => publishBlocker.GetPreferredArea(e)).OrderBy(g => g.Key).ToList(); + } + private static void RenderDetailedEntriesFlattened( StringBuilder sb, List entries, @@ -831,7 +861,7 @@ private static void RenderSeparatedTypeEntries( { if (dropdownsEnabled) { - RenderDetailedEntries(sb, entries, repo, owner, groupBySubtype, hideLinks, hideEntryDescriptions, publishBlocker); + RenderDetailedEntries(sb, entries, repo, owner, groupBySubtype, hideLinks, hideEntryDescriptions, subsections, publishBlocker); return; } diff --git a/tests/Elastic.Markdown.Tests/Directives/ChangelogDropdownsTests.cs b/tests/Elastic.Markdown.Tests/Directives/ChangelogDropdownsTests.cs index e0cb6fe228..3987157ebf 100644 --- a/tests/Elastic.Markdown.Tests/Directives/ChangelogDropdownsTests.cs +++ b/tests/Elastic.Markdown.Tests/Directives/ChangelogDropdownsTests.cs @@ -10,9 +10,9 @@ namespace Elastic.Markdown.Tests.Directives; /// /// Tests for the :dropdowns: parameter on the changelog directive. -/// By default (omitted), separated types (breaking changes, deprecations, known issues, highlights) -/// are rendered as flattened bulleted lists. -/// With :dropdowns:, they render as Myst dropdown sections. +/// By default (omitted), separated types (breaking changes, deprecations, known issues, highlights) +/// are rendered as flattened bulleted lists; Features stay flat bullets. +/// With :dropdowns:, those types and Features render as Myst dropdown sections. /// public class ChangelogDropdownsDefaultTests : DirectiveTest { @@ -408,15 +408,12 @@ public ChangelogDropdownsExplicitWithDifferentTypesTests(ITestOutputHelper outpu [Fact] public void ExplicitDropdownsRendersMixedTypesCorrectly() { - // Regular types should still render as bulleted lists (unchanged behavior) - Html.Should().Contain("Feature addition."); // Regular feature type (in
  • tags) - - // Separated types should render as dropdowns (explicit :dropdowns:) Html.Should().Contain("
    "); + Html.Should().Contain("Feature addition."); Html.Should().Contain("Breaking API change."); Html.Should().Contain("Known issue with search."); - // Should NOT contain flattened format for separated types (check they're in dropdown, not flat list) + Html.Should().NotContain("
  • Feature addition."); Html.Should().NotContain("

  • Breaking API change."); Html.Should().NotContain("

  • Known issue with search."); } @@ -468,3 +465,255 @@ public void ChangelogDropdownTitleStripsBackticksInHtml() Html.Should().NotContain("`ElasticAgentVersion`"); } } + +public class ChangelogDropdownsDefaultFeaturesStayListTests : DirectiveTest +{ + public ChangelogDropdownsDefaultFeaturesStayListTests(ITestOutputHelper output) : base( + output, + """ + :::{changelog} + :description-visibility: keep-descriptions + ::: + """ + ) => + FileSystem.AddFile( + "docs/changelog/bundles/9.3.0.yaml", + new MockFileData( + """ + products: + - product: elasticsearch + target: 9.3.0 + entries: + - title: Feature addition + type: feature + products: + - product: elasticsearch + target: 9.3.0 + description: FEATURE_FLAT_BODY + prs: + - "111111" + """ + ) + ); + + [Fact] + public void FeaturesStayBulletsWithoutDropdownsOption() + { + Html.Should().NotContain("

    "); + Html.Should().Contain("
  • "); + Html.Should().Contain("Feature addition"); + Html.Should().Contain("FEATURE_FLAT_BODY"); + } +} + +public class ChangelogDropdownsFeaturesNotEnhancementsTests : DirectiveTest +{ + public ChangelogDropdownsFeaturesNotEnhancementsTests(ITestOutputHelper output) : base( + output, + """ + :::{changelog} + :dropdowns: + :description-visibility: keep-feature-descriptions + ::: + """ + ) => + FileSystem.AddFile( + "docs/changelog/bundles/9.3.0.yaml", + new MockFileData( + """ + products: + - product: elasticsearch + target: 9.3.0 + entries: + - title: Feature addition + type: feature + products: + - product: elasticsearch + target: 9.3.0 + description: FEATURE_DROPDOWN_BODY + prs: + - "111111" + - title: Faster existing query + type: enhancement + products: + - product: elasticsearch + target: 9.3.0 + description: ENHANCEMENT_LIST_BODY + prs: + - "222222" + """ + ) + ); + + [Fact] + public void FeaturesRenderAsDropdowns() + { + Html.Should().Contain("
    "); + Html.Should().Contain("Feature addition"); + Html.Should().Contain("FEATURE_DROPDOWN_BODY"); + Html.Should().NotContain("
  • Feature addition"); + } + + [Fact] + public void EnhancementsStayBullets() + { + Html.Should().Contain("Faster existing query"); + Html.Should().NotContain("ENHANCEMENT_LIST_BODY"); + } +} + +public class ChangelogDropdownsFeaturesAutoHidesDescriptionTests : DirectiveTest +{ + public ChangelogDropdownsFeaturesAutoHidesDescriptionTests(ITestOutputHelper output) : base( + output, + """ + :::{changelog} + :dropdowns: + ::: + """ + ) => + FileSystem.AddFile( + "docs/changelog/bundles/9.3.0.yaml", + new MockFileData( + """ + products: + - product: elasticsearch + target: 9.3.0 + entries: + - title: Feature addition + type: feature + products: + - product: elasticsearch + target: 9.3.0 + description: FEATURE_AUTO_HIDDEN_BODY + prs: + - "111111" + """ + ) + ); + + [Fact] + public void RendersFeatureDropdownWithoutDescription() + { + Html.Should().Contain("

    "); + Html.Should().Contain("Feature addition"); + Html.Should().Contain("#111111"); + Html.Should().NotContain("FEATURE_AUTO_HIDDEN_BODY"); + } +} + +public class ChangelogDropdownsWithoutSubsectionsOmitsAreaHeadersTests : DirectiveTest +{ + public ChangelogDropdownsWithoutSubsectionsOmitsAreaHeadersTests(ITestOutputHelper output) : base( + output, + """ + :::{changelog} + :dropdowns: + :description-visibility: keep-feature-descriptions + ::: + """ + ) => + FileSystem.AddFile( + "docs/changelog/bundles/9.3.0.yaml", + new MockFileData( + """ + products: + - product: elasticsearch + target: 9.3.0 + entries: + - title: Feature in Search + type: feature + products: + - product: elasticsearch + target: 9.3.0 + areas: + - Search + description: Search feature body + prs: + - "111111" + - title: Feature in Indexing + type: feature + products: + - product: elasticsearch + target: 9.3.0 + areas: + - Indexing + description: Indexing feature body + prs: + - "222222" + """ + ) + ); + + [Fact] + public void RendersDropdowns() => Html.Should().Contain("
    "); + + [Fact] + public void OmitsAreaHeaders() + { + Html.Should().NotContain("Search"); + Html.Should().NotContain("Indexing"); + } + + [Fact] + public void StillRendersFeatureTitles() + { + Html.Should().Contain("Feature in Search"); + Html.Should().Contain("Feature in Indexing"); + } +} + +public class ChangelogDropdownsWithSubsectionsRendersAreaHeadersTests : DirectiveTest +{ + public ChangelogDropdownsWithSubsectionsRendersAreaHeadersTests(ITestOutputHelper output) : base( + output, + """ + :::{changelog} + :dropdowns: + :subsections: + :description-visibility: keep-feature-descriptions + ::: + """ + ) => + FileSystem.AddFile( + "docs/changelog/bundles/9.3.0.yaml", + new MockFileData( + """ + products: + - product: elasticsearch + target: 9.3.0 + entries: + - title: Feature in Search + type: feature + products: + - product: elasticsearch + target: 9.3.0 + areas: + - Search + description: Search feature body + prs: + - "111111" + - title: Feature in Indexing + type: feature + products: + - product: elasticsearch + target: 9.3.0 + areas: + - Indexing + description: Indexing feature body + prs: + - "222222" + """ + ) + ); + + [Fact] + public void RendersDropdowns() => Html.Should().Contain("
    "); + + [Fact] + public void RendersAreaHeaders() + { + Html.Should().Contain("Search"); + Html.Should().Contain("Indexing"); + } +}