diff --git a/tests/Elastic.Markdown.Tests/.snapshots/FileInclusion/IncludeHeadingOrderTests/IncludesSnippetAfterMainContent.verified.html b/tests/Elastic.Markdown.Tests/.snapshots/FileInclusion/IncludeHeadingOrderTests/IncludesSnippetAfterMainContent.verified.html new file mode 100644 index 0000000000..82f8393297 --- /dev/null +++ b/tests/Elastic.Markdown.Tests/.snapshots/FileInclusion/IncludeHeadingOrderTests/IncludesSnippetAfterMainContent.verified.html @@ -0,0 +1,40 @@ +
+

+ One +

+
+
+

+ Two +

+
+
+

+ Three +

+
+
+

+ Four +

+
+
+

+ Five +

+
+
+

+ Six +

+
+
+

+ Seven +

+
+
+

+ Eight +

+
diff --git a/tests/Elastic.Markdown.Tests/.snapshots/FileInclusion/IncludedAppliesSwitchTests/EachIncludeHasUniqueIds.verified.html b/tests/Elastic.Markdown.Tests/.snapshots/FileInclusion/IncludedAppliesSwitchTests/EachIncludeHasUniqueIds.verified.html new file mode 100644 index 0000000000..9b6a458501 --- /dev/null +++ b/tests/Elastic.Markdown.Tests/.snapshots/FileInclusion/IncludedAppliesSwitchTests/EachIncludeHasUniqueIds.verified.html @@ -0,0 +1,41 @@ +
+ + +
+

Content for Stack

+
+ + +
+

Content for Serverless

+
+
+

Some content between includes.

+
+ + +
+

Content for Stack

+
+ + +
+

Content for Serverless

+
+
diff --git a/tests/Elastic.Markdown.Tests/.snapshots/FileInclusion/IncludedTabSetTests/EachIncludeHasUniqueIds.verified.html b/tests/Elastic.Markdown.Tests/.snapshots/FileInclusion/IncludedTabSetTests/EachIncludeHasUniqueIds.verified.html new file mode 100644 index 0000000000..3861e29398 --- /dev/null +++ b/tests/Elastic.Markdown.Tests/.snapshots/FileInclusion/IncludedTabSetTests/EachIncludeHasUniqueIds.verified.html @@ -0,0 +1,25 @@ +
+ + +
+

Content for first tab

+
+ + +
+

Content for second tab

+
+
+

Some content between includes.

+
+ + +
+

Content for first tab

+
+ + +
+

Content for second tab

+
+
diff --git a/tests/Elastic.Markdown.Tests/.snapshots/Inline/FootnotesCodeBlockInDirectiveTests/CodeBlockRendered.verified.html b/tests/Elastic.Markdown.Tests/.snapshots/Inline/FootnotesCodeBlockInDirectiveTests/CodeBlockRendered.verified.html new file mode 100644 index 0000000000..d07f4cf799 --- /dev/null +++ b/tests/Elastic.Markdown.Tests/.snapshots/Inline/FootnotesCodeBlockInDirectiveTests/CodeBlockRendered.verified.html @@ -0,0 +1,38 @@ +
+ + +
+

Here's a simple footnote + 1 + + and another one + 2 + .

+
+ + +
+
+
+
+					Here's a simple footnote[^1] and another one[^2].  [^1]: This is the first footnote. [^2]: This is the second footnote.
+					
+				
+
+
+
+
+
+
+

Footnotes

+
    +
  1. +

    This is the first footnote.↩ +

    +
  2. +
  3. +

    This is the second footnote.↩ +

    +
  4. +
+
diff --git a/tests/Elastic.Markdown.Tests/.snapshots/Inline/FootnotesConsecutiveDefinitionsTests/GeneratesHtml.verified.html b/tests/Elastic.Markdown.Tests/.snapshots/Inline/FootnotesConsecutiveDefinitionsTests/GeneratesHtml.verified.html new file mode 100644 index 0000000000..ce67e419b8 --- /dev/null +++ b/tests/Elastic.Markdown.Tests/.snapshots/Inline/FootnotesConsecutiveDefinitionsTests/GeneratesHtml.verified.html @@ -0,0 +1,25 @@ +

First + 1 + , second + 2 + , third + 3 + .

+
+
+

Footnotes

+
    +
  1. +

    First footnote.↩ +

    +
  2. +
  3. +

    Second footnote.↩ +

    +
  4. +
  5. +

    Third footnote.↩ +

    +
  6. +
+
diff --git a/tests/Elastic.Markdown.Tests/.snapshots/Inline/FootnotesInsideDirectiveTests/GeneratesHtml.verified.html b/tests/Elastic.Markdown.Tests/.snapshots/Inline/FootnotesInsideDirectiveTests/GeneratesHtml.verified.html new file mode 100644 index 0000000000..3558917aa1 --- /dev/null +++ b/tests/Elastic.Markdown.Tests/.snapshots/Inline/FootnotesInsideDirectiveTests/GeneratesHtml.verified.html @@ -0,0 +1,35 @@ +
+ + +
+

Here's a + bold + and a + link + and footnote + 1 + .

+
+ + +
+
+
+
+					Here's footnote[^1].  [^1]: Example definition in code block.
+					
+				
+
+
+
+
+
+
+

Footnotes

+
    +
  1. +

    Footnote definitions must be at the document level, not inside directives.↩ +

    +
  2. +
+
diff --git a/tests/Elastic.Markdown.Tests/.snapshots/SettingsInclusion/DeploymentFilterEchOnKibanaGeneralSettings/GeneratesHtml.verified.html b/tests/Elastic.Markdown.Tests/.snapshots/SettingsInclusion/DeploymentFilterEchOnKibanaGeneralSettings/GeneratesHtml.verified.html new file mode 100644 index 0000000000..d3dbed58e5 --- /dev/null +++ b/tests/Elastic.Markdown.Tests/.snapshots/SettingsInclusion/DeploymentFilterEchOnKibanaGeneralSettings/GeneratesHtml.verified.html @@ -0,0 +1,1020 @@ +

Use these settings to configure general features available in Kibana.

+

General settings

+
+
execution_context.enabled + + +
+
+

+ Supported on: + + + + +

+

Propagate request-specific metadata to Elasticsearch server by way of the + x-opaque-id + header. It is available in {{ecloud}} 8.1.0 and later versions.

+

+ Datatype: + bool +

+
+
+

Elasticsearch connection settings

+
+
elasticsearch.compression + + +
+
+

+ Supported on: + + + + +

+

Specifies whether {{kib}} should use compression for communications with {{es}}. It is available in {{ecloud}} 8.3.0 and later versions.

+

+ Datatype: + bool +

+

+ Default: + false +

+
+
elasticsearch.maxSockets + + +
+
+

+ Supported on: + + + + +

+

The maximum number of sockets that can be used for communications with {{es}}. It is available in {{ecloud}} 8.2.0 and later versions.

+

+ Datatype: + int +

+

+ Default: + Infinity +

+
+
+

Logging settings

+
+
logging.root.level + + +
+
+

+ Supported on: + + + + +

+

Level at which a log record should be logged. Supported levels are: + all, + fatal, + error, + warn, + info, + debug, + trace, + off. Levels are ordered from + all + (highest) to + off + and a log record will be logged it its level is higher than or equal to the level of its logger, otherwise the log record is ignored. Use this value to + change the overall log level.

+

+ Datatype: + enum +

+

+ Default: + info +

+

+ Options: +

+ +
+
+ Tip +
+
+

Set to + all + to log all events, including system usage information and all requests. Set to + off + to silence all logs. You can also use the logging + cli commands + to set log level to + verbose + or silence all logs.

+
+
+

The following example shows a valid verbose + logging.root + configuration:

+
+
+
+					logging:   appenders:     console_appender:       type: console       layout:         type: pattern         highlight: true   root:     appenders: [console_appender]     level: all
+					
+				
+
+
+
+
+

Map settings

+
+
map.includeElasticMapsService + + +
+
+

+ Supported on: + + + + +

+

Set to + false + to disable connections to Elastic Maps Service. When + includeElasticMapsService + is turned off, only tile layer configured by + + map.tilemap.url + + is available in + Maps.

+

+ Datatype: + bool +

+

+ Default: + true +

+
+
map.tilemap.options.attribution + + +
+
+

+ Supported on: + + + + +

+

The map attribution string. Provide attributions in markdown and use + \| + to delimit attributions, for example: + "[attribution 1](https://www.attribution1)\|[attribution 2](https://www.attribution2)".

+

+ Datatype: + string +

+

+ Default: + "© [Elastic Maps Service](https://www.elastic.co/elastic-maps-service)" +

+
+
map.tilemap.options.maxZoom + + +
+
+

+ Supported on: + + + + +

+

The maximum zoom level.

+

+ Datatype: + int +

+

+ Default: + 10 +

+
+
map.tilemap.options.minZoom + + +
+
+

+ Supported on: + + + + +

+

The minimum zoom level.

+

+ Datatype: + int +

+

+ Default: + 1 +

+
+
map.tilemap.options.subdomains + + +
+
+

+ Supported on: + + + + +

+

An array of subdomains used by the tile service. Specify the position of the subdomain the URL with the token + {{s}}.

+

+ Datatype: + string +

+
+
map.tilemap.url + + +
+
+

+ Supported on: + + + + +

+

The URL to the service that {{kib}} uses as the default basemap in + maps + and + vega maps. By default, {{kib}} sets a basemap from the + Elastic Maps Service, but users can point to their own Tile Map Service. For example: + "https://tiles.elastic.co/v2/default/{{z}}/{{x}}/{{y}}.png?elastic_tile_service_tos=agree&my_app_name=kibana" +

+

+ Datatype: + string +

+
+
+

Migrations settings

+
+
migrations.batchSize + + +
+
+

+ Supported on: + + + + +

+

Defines the number of documents migrated at a time. The higher the value, the faster the Saved Objects migration process performs at the cost of higher memory consumption. If upgrade migrations results in {{kib}} crashing with an out of memory exception or fails due to an Elasticsearch + circuit_breaking_exception, use a smaller + batchSize + value to reduce the memory pressure.

+

+ Datatype: + int +

+

+ Default: + 1000 +

+
+
migrations.discardUnknownObjects + + +
+
+

+ Supported on: + + + + +

+

Discard saved objects with unknown types during a migration. Must be set to the target version, for example: + 8.4.0. Default: undefined. It is available in {{ecloud}} 8.4.0 and later versions.

+

+ Datatype: + string +

+
+
migrations.discardCorruptObjects + + +
+
+

+ Supported on: + + + + +

+

Discard corrupt saved objects, as well as those that cause transform errors during a migration. Must be set to the target version, for example: + 8.4.0. Default: undefined. It is available in {{ecloud}} 8.4.0 and later versions.

+

+ Datatype: + string +

+
+
+

Search and autocomplete settings

+
+
data.autocomplete.valueSuggestions.terminateAfter + + +
+
+

+ Supported on: + + + + +

+

Specifies the max number of documents loaded by each shard to generate autocomplete suggestions. Allowed values are between 1 and 10000000.

+

+ Datatype: + int +

+

+ Default: + 100000 +

+
+
data.autocomplete.valueSuggestions.timeout + + +
+
+

+ Supported on: + + + + +

+

Specifies the time in milliseconds to wait for autocomplete suggestions from Elasticsearch. Allowed values are between 1 and 1200000.

+

+ Datatype: + int +

+

+ Default: + 1000 +

+
+
unifiedSearch.autocomplete.valueSuggestions.timeout + + +
+
+

+ Supported on: + + + + +

+

Time in milliseconds to wait for autocomplete suggestions from {{es}}. This value must be a whole number greater than zero.

+

+ Datatype: + int +

+

+ Default: + 1000 +

+
+
unifiedSearch.autocomplete.valueSuggestions.terminateAfter + + +
+
+

+ Supported on: + + + + +

+

Maximum number of documents loaded by each shard to generate autocomplete suggestions. This value must be a whole number greater than zero.

+

+ Datatype: + int +

+

+ Default: + 100000 +

+
+
+ Note +
+
+

To reload the logging settings, send a SIGHUP signal to {{kib}}. For more logging configuration options, see the + Configure Logging in {{kib}} + guide.

+
+
+
+
+

Server settings

+
+
server.maxPayload + + +
+
+

+ Supported on: + + + + +

+

The maximum payload size in bytes for incoming server requests.

+

+ Datatype: + int +

+

+ Default: + 1048576 +

+
+
server.customResponseHeaders + + +
+
+

+ Supported on: + + + + +

+

Header names and values to send on all responses to the client from the {{kib}} server.

+

+ Datatype: + string +

+

+ Default: + {} +

+
+
+

Server compression settings

+
+
server.compression.brotli.enabled + + +
+
+

+ Supported on: + + + + +

+

Set to + true + to enable brotli (br) compression format. Browsers not supporting brotli compression will fallback to using gzip instead. This setting may not be used when + + server.compression.enabled + + is set to + false. It is available in {{ecloud}} 8.6.0 and later versions.

+

+ Datatype: + bool +

+

+ Default: + false +

+
+
+

Server security response headers

+
+
server.securityResponseHeaders.strictTransportSecurity + + +
+
+

+ Supported on: + + + + +

+

Controls whether the + + Strict-Transport-Security + + header is used in all responses to the client from the {{kib}} server, and specifies what value is used. Allowed values are any text value or + null. To disable, set to + null.

+

+ Datatype: + string +

+

+ Default: + null +

+
+
server.securityResponseHeaders.xContentTypeOptions + + +
+
+

+ Supported on: + + + + +

+

Controls whether the + + X-Content-Type-Options + + header is used in all responses to the client from the {{kib}} server, and specifies what value is used. Allowed values are + nosniff + or + null. To disable, set to + null.

+

+ Datatype: + string +

+

+ Default: + "nosniff" +

+
+
server.securityResponseHeaders.referrerPolicy + + +
+
+

+ Supported on: + + + + +

+

Controls whether the + + Referrer-Policy + + header is used in all responses to the client from the {{kib}} server, and specifies what value is used. Allowed values are + no-referrer, + no-referrer-when-downgrade, + origin, + origin-when-cross-origin, + same-origin, + strict-origin, + strict-origin-when-cross-origin, + unsafe-url, or + null. To disable, set to + null.

+

+ Datatype: + string +

+

+ Default: + "strict-origin-when-cross-origin" +

+
+
server.securityResponseHeaders.permissionsPolicy + + + +
+
+

+ Supported on: + + + + +

+

Controls whether the + + Permissions-Policy + + header is used in all responses to the client from the {{kib}} server, and specifies what value is used. Allowed values are any text value or + null. Refer to the + + Permissions-Policy + documentation + for defined directives, values, and text format. To disable, set to + null.

+

+ Datatype: + string +

+

+ Default: + camera=(), display-capture=(), fullscreen=(self), geolocation=(), microphone=(), web-share=() +

+
+
server.securityResponseHeaders.permissionsPolicyReportOnly + + + +
+
+

+ Supported on: + + + + +

+

Controls whether the + + Permissions-Policy-Report-Only + + header is used in all responses to the client from the {{kib}} server, and specifies what value is used. Allowed values are any text value or + null. Refer to the + + Permissions-Policy + documentation + for defined directives, values, and text format.

+

+ Datatype: + string +

+
+
server.securityResponseHeaders.disableEmbedding + + +
+
+

+ Supported on: + + + + +

+

Controls whether the + + Content-Security-Policy + + and + + X-Frame-Options + + headers are configured to disable embedding {{kib}} in other webpages using iframes. When set to + true, secure headers are used to disable embedding, which adds the + frame-ancestors: 'self' + directive to the + Content-Security-Policy + response header and adds the + X-Frame-Options: SAMEORIGIN + response header.

+

+ Datatype: + bool +

+

+ Default: + false +

+
+
server.securityResponseHeaders.crossOriginOpenerPolicy + + +
+
+

+ Supported on: + + + + +

+

Controls whether the + + Cross-Origin-Opener-Policy + + header is used in all responses to the client from the {{kib}} server, and specifies what value is used. Allowed values are + unsafe-none, + same-origin-allow-popups, + same-origin, or + null. To disable, set to + null. It is available in {{ecloud}} 8.7.0 and later versions.

+

+ Datatype: + enum +

+

+ Default: + "same-origin" +

+

+ Options: +

+ +
+
+

Server XSRF settings

+
+
server.xsrf.allowlist + + +
+
+

+ Supported on: + + + + +

+

It is not recommended to disable protections for arbitrary API endpoints. Instead, supply the + kbn-xsrf + header. The + + server.xsrf.allowlist + + setting requires the following format:

+
+
+
+					*Default: [ ]* An array of API endpoints which should be exempt from Cross-Site Request Forgery ("XSRF") protections.
+					
+				
+
+
+

It is available in {{ecloud}} 8.0.0 and later versions.

+

+ Datatype: + string +

+

+ Default: + [] +

+
+
+

UI and visualization settings

+
+
uiSettings.globalOverrides.hideAnnouncements + + + +
+
+

Set to + true + to stop showing messages and tours that highlight new features.

+

+ Datatype: + bool +

+

+ Default: + false +

+
+
uiSettings.globalOverrides.hideFeedback + + + +
+
+

Set to + true + to stop showing elements requesting user feedback.

+

+ Datatype: + bool +

+

+ Default: + false +

+
+
vis_type_table.legacyVisEnabled + + +
+
+

+ Supported on: + + + + +

+

Starting from version 7.11, a new datatable visualization is used. Set to + true + to enable the legacy version. In version 8.0 and later, the old implementation is removed and this setting is no longer supported.

+

+ Datatype: + bool +

+
+
vis_type_vega.enable + + +
+
+

+ Supported on: + + + + +

+

For 7.7 version and later, set to + false + to disable Vega vizualizations.

+

+ Datatype: + bool +

+

+ Default: + true +

+
+
vega.enableExternalUrls + + + +
+
+

+ Supported on: + + + + +

+

Set to + true + to allow Vega vizualizations to use data from sources other than the linked Elasticsearch cluster. In version 8.0 and later, the + vega.enableExternalUrls + is not supported. Use + vis_type_vega.enableExternalUrls + instead.

+

+ Datatype: + bool +

+
+
+ Deprecation details +
+
+

In version 8.0 and later, this setting is not supported. Use + vis_type_vega.enableExternalUrls + instead.

+
+
+
+
vis_type_vega.enableExternalUrls + + +
+
+

+ Supported on: + + + + +

+

Set this value to true to allow Vega to use any URL to access external data sources and images. When false, Vega can only get data from {{es}}.

+

+ Datatype: + bool +

+

+ Default: + false +

+
+
+

Feature and plugin settings

+
+
xpack.securitySolution.maxUploadResponseActionFileBytes + + +
+
+

+ Supported on: + + + + +

+

Allow to configure the max file upload size for use with the Upload File Response action available with the Defend Integration. To learn more, check + Endpoint Response actions. It is available in {{ecloud}} 8.9.0 and later versions.

+

+ Datatype: + string +

+
+
xpack.securitySolution.disableEndpointRuleAutoInstall + + + +
+
+

Set to + true + to disable the automatic installation of Elastic Defend SIEM rules when a new Endpoint integration policy is created. Introduced with v9.2.4.

+

+ Datatype: + bool +

+

+ Default: + false +

+
+
diff --git a/tests/Elastic.Markdown.Tests/.snapshots/SettingsInclusion/DeploymentFilterWithUnknownValueEmitsWarning/StillRendersAllSettingsWhenFilterIsInvalid.verified.html b/tests/Elastic.Markdown.Tests/.snapshots/SettingsInclusion/DeploymentFilterWithUnknownValueEmitsWarning/StillRendersAllSettingsWhenFilterIsInvalid.verified.html new file mode 100644 index 0000000000..ec59d86f70 --- /dev/null +++ b/tests/Elastic.Markdown.Tests/.snapshots/SettingsInclusion/DeploymentFilterWithUnknownValueEmitsWarning/StillRendersAllSettingsWhenFilterIsInvalid.verified.html @@ -0,0 +1,3773 @@ +

Use these settings to configure general features available in Kibana.

+

General settings

+
+
console.ui.enabled + + +
+
+

+ Supported on: + + + +

+

Toggling this causes the server to regenerate assets on the next startup, which may cause a delay before pages start being served. Set to + false + to disable Console.

+

+ Datatype: + bool +

+

+ Default: + true +

+
+
execution_context.enabled + + +
+
+

+ Supported on: + + + + +

+

Propagate request-specific metadata to Elasticsearch server by way of the + x-opaque-id + header. It is available in {{ecloud}} 8.1.0 and later versions.

+

+ Datatype: + bool +

+
+
newsfeed.enabled + + +
+
+

+ Supported on: + + + +

+

Controls whether to enable the newsfeed system for the {{kib}} UI notification center. Set to + false + to disable the newsfeed system.

+

+ Datatype: + bool +

+

+ Default: + true +

+
+
node.roles + + + +
+
+

+ Supported on: + + + +

+

Indicates which roles to configure the {{kib}} process with, which will effectively run {{kib}} in different modes. Valid options are + background_tasks + and + ui, or + * + to select all roles.

+

+ Datatype: + string +

+

+ Default: + * +

+
+
notifications.connectors.default.email + + +
+
+

+ Supported on: + + + +

+

Choose the default email connector for user notifications. As of + 8.6.0, {{kib}} is shipping with a new notification mechanism that will send email notifications for various user actions, e.g. assigning a + Case + to a user. To enable notifications, an email connector must be + preconfigured + in the system via + kibana.yml, and the notifications plugin must be configured to point to the ID of that connector.

+

+ Datatype: + string +

+
+
ops.interval + + +
+
+

+ Supported on: + + + +

+

Set the interval in milliseconds to sample system and process performance metrics. The minimum value is 100.

+

+ Datatype: + int +

+

+ Default: + 5000 +

+
+
ops.cGroupOverrides.cpuPath + + +
+
+

+ Supported on: + + + +

+

Override for cgroup cpu path when mounted in a manner that is inconsistent with + /proc/self/cgroup.

+

+ Datatype: + string +

+
+
ops.cGroupOverrides.cpuAcctPath + + +
+
+

+ Supported on: + + + +

+

Override for cgroup cpuacct path when mounted in a manner that is inconsistent with + /proc/self/cgroup.

+

+ Datatype: + string +

+
+
path.data + + +
+
+

+ Supported on: + + + +

+

The path where {{kib}} stores persistent data not saved in {{es}}.

+

+ Datatype: + string +

+

+ Default: + data +

+
+
permissionsPolicy.report_to + + +
+
+

+ Supported on: + + + +

+

Add sources for the + Permissions Policy + report-to + directive.

+

+ Datatype: + string +

+
+
pid.file + + +
+
+

+ Supported on: + + + +

+

Specifies the path where {{kib}} creates the process ID file.

+

+ Datatype: + string +

+
+
status.allowAnonymous + + +
+
+

+ Supported on: + + + +

+

If authentication is enabled, setting this to + true + enables unauthenticated users to access the {{kib}} server status API and status page.

+

+ Datatype: + bool +

+

+ Default: + false +

+
+
+

Content Security Policy (CSP) settings

+
+
csp.script_src + + +
+
+

+ Supported on: + + + +

+

Add sources for the + Content Security Policy + script-src + directive.

+

+ Datatype: + string +

+
+
csp.disableUnsafeEval + + + +
+
+

Set this to + false + to add the + + unsafe-eval + + source expression to the + script-src + directive.

+

When + csp.disableUnsafeEval + is set to + true, Kibana will use a custom version of the Handlebars template library. Handlebars is used in various locations in the Kibana frontend where custom templates can be supplied by the user when for instance setting up a visualisation. If you experience any issues rendering Handlebars templates, please set this setting to + false + and + open an issue + in the Kibana GitHub repository.

+

+ Datatype: + bool +

+

+ Default: + true +

+
+
+ Deprecation details +
+
+

Deprecated in 8.7.0. Use + csp.script_src: ['unsafe-eval'] + instead if you wish to enable + unsafe-eval. This config option will have no effect in a future version.

+
+
+
+
csp.worker_src + + +
+
+

+ Supported on: + + + +

+

Add sources for the + Content Security Policy + worker-src + directive.

+

+ Datatype: + string +

+
+
csp.style_src + + +
+
+

+ Supported on: + + + +

+

Add sources for the + Content Security Policy + style-src + directive.

+

+ Datatype: + string +

+
+
csp.connect_src + + +
+
+

+ Supported on: + + + +

+

Add sources for the + Content Security Policy + connect-src + directive.

+

+ Datatype: + string +

+
+
csp.default_src + + +
+
+

+ Supported on: + + + +

+

Add sources for the + Content Security Policy + default-src + directive.

+

+ Datatype: + string +

+
+
csp.font_src + + +
+
+

+ Supported on: + + + +

+

Add sources for the + Content Security Policy + font-src + directive.

+

+ Datatype: + string +

+
+
csp.frame_src + + +
+
+

+ Supported on: + + + +

+

Add sources for the + Content Security Policy + frame-src + directive.

+

+ Datatype: + string +

+
+
csp.img_src + + +
+
+

+ Supported on: + + + +

+

Add sources for the + Content Security Policy + img-src + directive.

+

+ Datatype: + string +

+
+
csp.object_src + + + +
+
+

Add sources for the + Content Security Policy + object-src + directive.

+

+ Datatype: + string +

+
+
csp.frame_ancestors + + +
+
+

+ Supported on: + + + +

+

Add sources for the + Content Security Policy + frame-ancestors + directive.

+

+ Datatype: + string +

+
+
+ Note +
+
+

The + frame-ancestors + directive can also be configured by using + + server.securityResponseHeaders.disableEmbedding + . In that case, that takes precedence and any values in + csp.frame_ancestors + are ignored.

+
+
+
+
csp.report_only.form_action + + +
+
+

+ Supported on: + + + +

+

Add sources for the + Content Security Policy + form-action + directive + in reporting mode.

+

+ Datatype: + string +

+
+
csp.report_only.object_src + + + +
+
+

Add sources for the + Content Security Policy + object-src + directive + in reporting mode.

+

+ Datatype: + string +

+
+
+ Deprecation details +
+
+

This setting is deprecated in favor of + csp.object_src.

+
+
+
+
csp.report_uri + + +
+
+

+ Supported on: + + + +

+

Add sources for the + Content Security Policy + report-uri + directive.

+

+ Datatype: + string +

+
+
csp.report_to + + +
+
+

+ Supported on: + + + +

+

Add sources for the + Content Security Policy + report-to + directive.

+

+ Datatype: + string +

+
+
csp.strict + + +
+
+

+ Supported on: + + + +

+

Blocks {{kib}} access to any browser that does not enforce even rudimentary CSP rules. In practice, this disables support for older, less safe browsers like Internet Explorer.

+

+ Datatype: + bool +

+

+ Default: + true +

+
+
csp.warnLegacyBrowsers + + +
+
+

+ Supported on: + + + +

+

Shows a warning message after loading {{kib}} to any browser that does not enforce even rudimentary CSP rules, though {{kib}} is still accessible. This configuration is effectively ignored when + + csp.strict + + is enabled.

+

+ Datatype: + bool +

+

+ Default: + true +

+
+
+

Elasticsearch connection settings

+
+
elasticsearch.customHeaders + + +
+
+

+ Supported on: + + + +

+

Header names and values to send to {{es}}. Any custom headers cannot be overwritten by client-side headers, regardless of the + + elasticsearch.requestHeadersWhitelist + + configuration.

+

+ Datatype: + string +

+

+ Default: + {} +

+
+
elasticsearch.hosts + + +
+
+

+ Supported on: + + + +

+

The URLs of the {{es}} instances to use for all your queries. All nodes listed here must be on the same cluster.

+

To enable SSL/TLS for outbound connections to {{es}}, use the + https + protocol in this setting.

+

+ Datatype: + string +

+

+ Default: + [ "http://localhost:9200" ] +

+
+
elasticsearch.publicBaseUrl + + +
+
+

+ Supported on: + + + +

+

The URL through which Elasticsearch is publicly accessible, if any. This will be shown to users in Kibana when they need connection details for your Elasticsearch cluster.

+

+ Datatype: + string +

+
+
elasticsearch.pingTimeout + + +
+
+

+ Supported on: + + + +

+

Time in milliseconds to wait for {{es}} to respond to pings. Defaults to the value of the + + elasticsearch.requestTimeout + + setting.

+

+ Datatype: + int +

+
+
elasticsearch.requestHeadersWhitelist + + +
+
+

+ Supported on: + + + +

+

List of {{kib}} client-side headers to send to {{es}}. To send + no + client-side headers, set this value to [] (an empty list). Removing the + authorization + header from being whitelisted means that you cannot use + basic authentication + in {{kib}}.

+

+ Datatype: + string +

+

+ Default: + [ 'authorization', 'es-client-authentication' ] +

+
+
elasticsearch.requestTimeout + + +
+
+

+ Supported on: + + + +

+

Time in milliseconds to wait for responses from the back end or {{es}}. This value must be a positive integer.

+

+ Datatype: + int +

+

+ Default: + 30000 +

+
+
elasticsearch.shardTimeout + + +
+
+

+ Supported on: + + + +

+

Time in milliseconds for {{es}} to wait for responses from shards. Set to 0 to disable.

+

+ Datatype: + int +

+

+ Default: + 30000 +

+
+
elasticsearch.compression + + +
+
+

+ Supported on: + + + + +

+

Specifies whether {{kib}} should use compression for communications with {{es}}. It is available in {{ecloud}} 8.3.0 and later versions.

+

+ Datatype: + bool +

+

+ Default: + false +

+
+
elasticsearch.sniffInterval + + +
+
+

+ Supported on: + + + +

+

Time in milliseconds between requests to check {{es}} for an updated list of nodes.

+

+ Datatype: + string +

+

+ Default: + false +

+
+
elasticsearch.sniffOnStart + + +
+
+

+ Supported on: + + + +

+

Attempt to find other {{es}} nodes on startup.

+

+ Datatype: + bool +

+

+ Default: + false +

+
+
elasticsearch.sniffOnConnectionFault + + +
+
+

+ Supported on: + + + +

+

Update the list of {{es}} nodes immediately following a connection fault.

+

+ Datatype: + bool +

+

+ Default: + false +

+
+
elasticsearch.maxSockets + + +
+
+

+ Supported on: + + + + +

+

The maximum number of sockets that can be used for communications with {{es}}. It is available in {{ecloud}} 8.2.0 and later versions.

+

+ Datatype: + int +

+

+ Default: + Infinity +

+
+
elasticsearch.maxResponseSize + + +
+
+

+ Supported on: + + + +

+

Either + false + or a + byteSize + value. When set, responses from {{es}} with a size higher than the defined limit will be rejected. This is intended to be used as a circuit-breaker mechanism to avoid memory errors in case of unexpectedly high responses coming from {{es}}.

+

+ Datatype: + string +

+

+ Default: + false +

+
+
elasticsearch.maxIdleSockets + + +
+
+

+ Supported on: + + + +

+

The maximum number of idle sockets to keep open between {{kib}} and {{es}}. If more sockets become idle, they will be closed.

+

+ Datatype: + int +

+

+ Default: + 256 +

+
+
elasticsearch.idleSocketTimeout + + +
+
+

+ Supported on: + + + +

+

The timeout for idle sockets kept open between {{kib}} and {{es}}. If the socket is idle for longer than this timeout, it will be closed. If you have a transparent proxy between {{kib}} and {{es}} be sure to set this value lower than or equal to the proxy's timeout.

+

+ Datatype: + string +

+

+ Default: + 60s +

+
+
elasticsearch.username + + +
+
+

+ Supported on: + + + +

+

If your {{es}} is protected with basic authentication, this setting provides the username that the {{kib}} server uses to perform maintenance on the {{kib}} index at startup. {{kib}} users still need to authenticate with {{es}}, which is proxied through the {{kib}} server.

+

+ Datatype: + string +

+
+
elasticsearch.password + + +
+
+

+ Supported on: + + + +

+

If your {{es}} is protected with basic authentication, this setting provides the password that the {{kib}} server uses to perform maintenance on the {{kib}} index at startup. {{kib}} users still need to authenticate with {{es}}, which is proxied through the {{kib}} server.

+

+ Datatype: + string +

+
+
elasticsearch.serviceAccountToken + + +
+
+

+ Supported on: + + + +

+

If your {{es}} is protected with basic authentication, this token provides the credentials that the {{kib}} server uses to perform maintenance on the {{kib}} index at startup. This setting is an alternative to + elasticsearch.username + and + elasticsearch.password.

+

+ Datatype: + string +

+
+
+

Elasticsearch SSL settings

+
+
elasticsearch.ssl.alwaysPresentCertificate + + +
+
+

+ Supported on: + + + +

+

Controls {{kib}} behavior in regard to presenting a client certificate when requested by {{es}}. This setting applies to all outbound SSL/TLS connections to {{es}}, including requests that are proxied for end users.

+

+ Datatype: + bool +

+

+ Default: + false +

+
+
+ Warning +
+
+

When {{es}} uses certificates to authenticate end users with a PKI realm and + + elasticsearch.ssl.alwaysPresentCertificate + + is + true, proxied requests may be executed as the identity that is tied to the {{kib}} server.

+
+
+
+
elasticsearch.ssl.certificate + + +
+
+

+ Supported on: + + + +

+

Path to a PEM-encoded X.509 client certificate. This is used by {{kib}} to authenticate itself when making outbound SSL/TLS connections to {{es}}. For this setting to take effect, the + xpack.security.http.ssl.client_authentication + setting in {{es}} must be also be set to + "required" + or + "optional" + to request a client certificate from {{kib}}.

+

+ Datatype: + string +

+
+
+ Note +
+
+

This setting cannot be used in conjunction with + + elasticsearch.ssl.keystore.path + .

+
+
+
+
elasticsearch.ssl.key + + +
+
+

+ Supported on: + + + +

+

Path to the corresponding private key for the PEM-encoded X.509 client certificate specified via + + elasticsearch.ssl.certificate + . These are used by {{kib}} to authenticate itself when making outbound SSL/TLS connections to {{es}}. For this setting to take effect, the + xpack.security.http.ssl.client_authentication + setting in {{es}} must be also be set to + "required" + or + "optional" + to request a client certificate from {{kib}}.

+

+ Datatype: + string +

+
+
+ Note +
+
+

This setting cannot be used in conjunction with + + elasticsearch.ssl.keystore.path + .

+
+
+
+
elasticsearch.ssl.certificateAuthorities + + +
+
+

+ Supported on: + + + +

+

Paths to one or more PEM-encoded X.509 certificate authority (CA) certificates, which make up a trusted certificate chain for {{es}}. This chain is used by {{kib}} to establish trust when making outbound SSL/TLS connections to {{es}}.

+

In addition to this setting, trusted certificates may be specified via + + elasticsearch.ssl.keystore.path + + and/or + + elasticsearch.ssl.truststore.path + .

+

+ Datatype: + string +

+
+
elasticsearch.ssl.keyPassphrase + + +
+
+

+ Supported on: + + + +

+

The password that decrypts the private key that is specified via + + elasticsearch.ssl.key + . This value is optional, as the key may not be encrypted.

+

+ Datatype: + string +

+
+
elasticsearch.ssl.keystore.path + + +
+
+

+ Supported on: + + + +

+

Path to a PKCS#12 keystore that contains an X.509 client certificate and it's corresponding private key. These are used by {{kib}} to authenticate itself when making outbound SSL/TLS connections to {{es}}. For this setting, you must also set the + xpack.security.http.ssl.client_authentication + setting in {{es}} to + "required" + or + "optional" + to request a client certificate from {{kib}}.

+

If the keystore contains any additional certificates, they are used as a trusted certificate chain for {{es}}. This chain is used by {{kib}} to establish trust when making outbound SSL/TLS connections to {{es}}. In addition to this setting, trusted certificates may be specified via + + elasticsearch.ssl.certificateAuthorities + + and/or + + elasticsearch.ssl.truststore.path + .

+

+ Datatype: + string +

+
+
+ Note +
+
+

This setting cannot be used in conjunction with + + elasticsearch.ssl.certificate + + or + + elasticsearch.ssl.key + .

+
+
+
+
elasticsearch.ssl.keystore.password + + +
+
+

+ Supported on: + + + +

+

The password that decrypts the keystore specified via + + elasticsearch.ssl.keystore.path + . If the keystore has no password, leave this as blank. If the keystore has an empty password, set this to + "".

+

+ Datatype: + string +

+
+
elasticsearch.ssl.truststore.path + + +
+
+

+ Supported on: + + + +

+

Path to a PKCS#12 trust store that contains one or more X.509 certificate authority (CA) certificates, which make up a trusted certificate chain for {{es}}. This chain is used by {{kib}} to establish trust when making outbound SSL/TLS connections to {{es}}.

+

In addition to this setting, trusted certificates may be specified via + + elasticsearch.ssl.certificateAuthorities + + and/or + + elasticsearch.ssl.keystore.path + .

+

+ Datatype: + string +

+
+
elasticsearch.ssl.truststore.password + + +
+
+

+ Supported on: + + + +

+

The password that decrypts the trust store specified via + + elasticsearch.ssl.truststore.path + . If the trust store has no password, leave this as blank. If the trust store has an empty password, set this to + "".

+

+ Datatype: + string +

+
+
elasticsearch.ssl.verificationMode + + +
+
+

+ Supported on: + + + +

+

Controls the verification of the server certificate that {{kib}} receives when making an outbound SSL/TLS connection to {{es}}. Valid values are + "full", + "certificate", and + "none". Using + "full" + performs hostname verification, using + "certificate" + skips hostname verification, and using + "none" + skips verification entirely.

+

+ Datatype: + enum +

+

+ Default: + full +

+

+ Options: +

+ +
+
+

Logging settings

+
+
logging.root + + +
+
+

+ Supported on: + + + +

+

The + root + logger is a + dedicated logger + and is pre-configured. The + root + logger logs at + info + level by default. If any other logging configuration is specified, + root + must + also be explicitly configured.

+

+ Datatype: + string +

+
+
logging.root.appenders + + +
+
+

+ Supported on: + + + +

+

A list of logging appenders to forward the root level logger instance to. By default + root + is configured with the + default + appender that logs to stdout with a + pattern + layout. This is the configuration that all custom loggers will use unless they're re-configured explicitly. You can override the default behavior by configuring a different + appender + to apply to + root.

+

+ Datatype: + string +

+
+
logging.root.level + + +
+
+

+ Supported on: + + + + +

+

Level at which a log record should be logged. Supported levels are: + all, + fatal, + error, + warn, + info, + debug, + trace, + off. Levels are ordered from + all + (highest) to + off + and a log record will be logged it its level is higher than or equal to the level of its logger, otherwise the log record is ignored. Use this value to + change the overall log level.

+

+ Datatype: + enum +

+

+ Default: + info +

+

+ Options: +

+ +
+
+ Tip +
+
+

Set to + all + to log all events, including system usage information and all requests. Set to + off + to silence all logs. You can also use the logging + cli commands + to set log level to + verbose + or silence all logs.

+
+
+

The following example shows a valid verbose + logging.root + configuration:

+
+
+
+					logging:   appenders:     console_appender:       type: console       layout:         type: pattern         highlight: true   root:     appenders: [console_appender]     level: all
+					
+				
+
+
+
+
logging.loggers[] + + +
+
+

+ Supported on: + + + +

+

Allows you to + customize a specific logger instance.

+

+ Datatype: + string +

+
+
logging.appenders[] + + +
+
+

+ Supported on: + + + +

+

+ Appenders + define how and where log messages are displayed (eg. + stdout + or console) and stored (eg. file on the disk).

+

+ Datatype: + string +

+
+
+

Map settings

+
+
map.includeElasticMapsService + + +
+
+

+ Supported on: + + + + +

+

Set to + false + to disable connections to Elastic Maps Service. When + includeElasticMapsService + is turned off, only tile layer configured by + + map.tilemap.url + + is available in + Maps.

+

+ Datatype: + bool +

+

+ Default: + true +

+
+
map.emsUrl + + +
+
+

+ Supported on: + + + +

+

Specifies the URL of a self hosted + {{hosted-ems}} +

+

+ Datatype: + string +

+
+
map.tilemap.options.attribution + + +
+
+

+ Supported on: + + + + +

+

The map attribution string. Provide attributions in markdown and use + \| + to delimit attributions, for example: + "[attribution 1](https://www.attribution1)\|[attribution 2](https://www.attribution2)".

+

+ Datatype: + string +

+

+ Default: + "© [Elastic Maps Service](https://www.elastic.co/elastic-maps-service)" +

+
+
map.tilemap.options.maxZoom + + +
+
+

+ Supported on: + + + + +

+

The maximum zoom level.

+

+ Datatype: + int +

+

+ Default: + 10 +

+
+
map.tilemap.options.minZoom + + +
+
+

+ Supported on: + + + + +

+

The minimum zoom level.

+

+ Datatype: + int +

+

+ Default: + 1 +

+
+
map.tilemap.options.subdomains + + +
+
+

+ Supported on: + + + + +

+

An array of subdomains used by the tile service. Specify the position of the subdomain the URL with the token + {{s}}.

+

+ Datatype: + string +

+
+
map.tilemap.url + + +
+
+

+ Supported on: + + + + +

+

The URL to the service that {{kib}} uses as the default basemap in + maps + and + vega maps. By default, {{kib}} sets a basemap from the + Elastic Maps Service, but users can point to their own Tile Map Service. For example: + "https://tiles.elastic.co/v2/default/{{z}}/{{x}}/{{y}}.png?elastic_tile_service_tos=agree&my_app_name=kibana" +

+

+ Datatype: + string +

+
+
+

Migrations settings

+
+
migrations.batchSize + + +
+
+

+ Supported on: + + + + +

+

Defines the number of documents migrated at a time. The higher the value, the faster the Saved Objects migration process performs at the cost of higher memory consumption. If upgrade migrations results in {{kib}} crashing with an out of memory exception or fails due to an Elasticsearch + circuit_breaking_exception, use a smaller + batchSize + value to reduce the memory pressure.

+

+ Datatype: + int +

+

+ Default: + 1000 +

+
+
migrations.discardUnknownObjects + + +
+
+

+ Supported on: + + + + +

+

Discard saved objects with unknown types during a migration. Must be set to the target version, for example: + 8.4.0. Default: undefined. It is available in {{ecloud}} 8.4.0 and later versions.

+

+ Datatype: + string +

+
+
migrations.discardCorruptObjects + + +
+
+

+ Supported on: + + + + +

+

Discard corrupt saved objects, as well as those that cause transform errors during a migration. Must be set to the target version, for example: + 8.4.0. Default: undefined. It is available in {{ecloud}} 8.4.0 and later versions.

+

+ Datatype: + string +

+
+
migrations.maxBatchSizeBytes + + +
+
+

+ Supported on: + + + +

+

Defines the maximum payload size for indexing batches of upgraded saved objects to avoid migrations failing due to a 413 Request Entity Too Large response from Elasticsearch. This value should be lower than or equal to your Elasticsearch cluster's + http.max_content_length + configuration option.

+

+ Datatype: + string +

+

+ Default: + 100mb +

+
+
migrations.retryAttempts + + +
+
+

+ Supported on: + + + +

+

The number of times migrations retry temporary failures, such as a network timeout, 503 status code, or + snapshot_in_progress_exception. When upgrade migrations frequently fail after exhausting all retry attempts with a message such as + Unable to complete the [...] step after 15 attempts, terminating., increase the setting value.

+

+ Datatype: + int +

+

+ Default: + 15 +

+
+
migrations.useCumulativeLogger + + + +
+
+

Skip logging migration progress unless there are any errors. Set to + false + when troubleshooting migration issues and not automatically shown.

+

+ Datatype: + bool +

+

+ Default: + true +

+
+
+

Saved Objects settings

+
+
savedObjects.maxImportExportSize + + +
+
+

+ Supported on: + + + +

+

The maximum count of saved objects that can be imported or exported. This setting exists to prevent the {{kib}} server from running out of memory when handling large numbers of saved objects. It is recommended to only raise this setting if you are confident your server can hold this many objects in memory.

+

+ Datatype: + int +

+

+ Default: + 10000 +

+
+
savedObjects.maxImportPayloadBytes + + +
+
+

+ Supported on: + + + +

+

The maximum byte size of a saved objects import that the {{kib}} server will accept. This setting exists to prevent the {{kib}} server from running out of memory when handling a large import payload. Note that this setting overrides the more general + + server.maxPayload + + for saved object imports only.

+

+ Datatype: + int +

+

+ Default: + 26214400 +

+
+
+

Search and autocomplete settings

+
+
data.autocomplete.valueSuggestions.terminateAfter + + +
+
+

+ Supported on: + + + + +

+

Specifies the max number of documents loaded by each shard to generate autocomplete suggestions. Allowed values are between 1 and 10000000.

+

+ Datatype: + int +

+

+ Default: + 100000 +

+
+
data.autocomplete.valueSuggestions.timeout + + +
+
+

+ Supported on: + + + + +

+

Specifies the time in milliseconds to wait for autocomplete suggestions from Elasticsearch. Allowed values are between 1 and 1200000.

+

+ Datatype: + int +

+

+ Default: + 1000 +

+
+
unifiedSearch.autocomplete.valueSuggestions.timeout + + +
+
+

+ Supported on: + + + + +

+

Time in milliseconds to wait for autocomplete suggestions from {{es}}. This value must be a whole number greater than zero.

+

+ Datatype: + int +

+

+ Default: + 1000 +

+
+
unifiedSearch.autocomplete.valueSuggestions.terminateAfter + + +
+
+

+ Supported on: + + + + +

+

Maximum number of documents loaded by each shard to generate autocomplete suggestions. This value must be a whole number greater than zero.

+

+ Datatype: + int +

+

+ Default: + 100000 +

+
+
+ Note +
+
+

To reload the logging settings, send a SIGHUP signal to {{kib}}. For more logging configuration options, see the + Configure Logging in {{kib}} + guide.

+
+
+
+
+

Server settings

+
+
server.basePath + + +
+
+

+ Supported on: + + + +

+

Enables you to specify a path to mount {{kib}} at if you are running behind a proxy. Use the + + server.rewriteBasePath + + setting to tell {{kib}} if it should remove the basePath from requests it receives, and to prevent a deprecation warning at startup. This setting cannot end in a slash (/).

+

+ Datatype: + string +

+
+
server.publicBaseUrl + + +
+
+

+ Supported on: + + + +

+

The publicly available URL that end-users access Kibana at. Must include the protocol, hostname, port (if different than the defaults for + http + and + https, 80 and 443 respectively), and the + + server.basePath + + (when that setting is configured explicitly). This setting cannot end in a slash (/).

+

+ Datatype: + string +

+
+
server.host + + +
+
+

+ Supported on: + + + +

+

This setting specifies the host of the back end server. To allow remote users to connect, set the value to the IP address or DNS name of the {{kib}} server. Use + 0.0.0.0 + to make Kibana listen on all IPs (public and private).

+

+ Datatype: + string +

+

+ Default: + "localhost" +

+
+
server.keepaliveTimeout + + +
+
+

+ Supported on: + + + +

+

The number of milliseconds to wait for additional data before restarting the + + server.socketTimeout + + counter.

+

+ Datatype: + int +

+

+ Default: + 120000 +

+
+
server.maxPayload + + +
+
+

+ Supported on: + + + + +

+

The maximum payload size in bytes for incoming server requests.

+

+ Datatype: + int +

+

+ Default: + 1048576 +

+
+
server.name + + +
+
+

+ Supported on: + + + +

+

A human-readable display name that identifies this {{kib}} instance.

+

+ Datatype: + string +

+

+ Default: + "your-hostname" +

+
+
server.port + + +
+
+

+ Supported on: + + + +

+

{{kib}} is served by a back end server. This setting specifies the port to use.

+

+ Datatype: + int +

+

+ Default: + 5601 +

+
+
server.protocol + + + +
+
+

+ Supported on: + + + +

+

The HTTP protocol to use, either + http1 + or + http2. Set to + http1 + to opt out of + HTTP/2 + support when TLS is enabled. Use of + http1 + may impact browser loading performance especially for dashboards with many panels. Default is + http2 + if TLS is enabled, otherwise + http1.

+

+ Datatype: + enum +

+

+ Options: +

+ +
+
+ Note +
+
+

By default, enabling + http2 + requires a valid + h2c + configuration, meaning that TLS must be enabled via + + server.ssl.enabled + + and + + server.ssl.supportedProtocols + , if specified, must contain at least + TLSv1.2 + or + TLSv1.3. Strict validation of the + h2c + setup can be disabled by adding + server.http2.allowUnsecure: true + to the configuration.

+
+
+
+
server.shutdownTimeout + + +
+
+

+ Supported on: + + + +

+

Sets the grace period for {{kib}} to attempt to resolve any ongoing HTTP requests after receiving a + SIGTERM/SIGINT + signal, and before shutting down. Any new HTTP requests received during this period are rejected, because the incoming socket is closed without further processing.

+

+ Datatype: + string +

+

+ Default: + 30s +

+
+
server.socketTimeout + + +
+
+

+ Supported on: + + + +

+

The number of milliseconds to wait before closing an inactive socket.

+

+ Datatype: + int +

+

+ Default: + 120000 +

+
+
server.payloadTimeout + + +
+
+

+ Supported on: + + + +

+

Sets the maximum time allowed for the client to transmit the request payload (body) before giving up and responding with a Request Timeout (408) error response.

+

+ Datatype: + int +

+

+ Default: + 20000 +

+
+
server.customResponseHeaders + + +
+
+

+ Supported on: + + + + +

+

Header names and values to send on all responses to the client from the {{kib}} server.

+

+ Datatype: + string +

+

+ Default: + {} +

+
+
server.requestId.allowFromAnyIp + + +
+
+

+ Supported on: + + + +

+

Sets whether or not the + X-Opaque-Id + header should be trusted from any IP address for identifying requests in logs and forwarded to Elasticsearch.

+

+ Datatype: + bool +

+
+
server.requestId.ipAllowlist + + +
+
+

+ Supported on: + + + +

+

A list of IPv4 and IPv6 address which the + X-Opaque-Id + header should be trusted from. Normally this would be set to the IP addresses of the load balancers or reverse-proxy that end users use to access Kibana. If any are set, + + server.requestId.allowFromAnyIp + + must also be set to + false. +

+

+ Datatype: + string +

+
+
server.rewriteBasePath + + +
+
+

+ Supported on: + + + +

+

Specifies whether {{kib}} should rewrite requests that are prefixed with + + server.basePath + + or require that they are rewritten by your reverse proxy.

+

+ Datatype: + bool +

+

+ Default: + false +

+
+
server.uuid + + +
+
+

+ Supported on: + + + +

+

The unique identifier for this {{kib}} instance. It must be a valid UUIDv4. It gets automatically generated on the first startup if not specified and persisted in the + data + path.

+

+ Datatype: + string +

+
+
+

Server compression settings

+
+
server.compression.enabled + + +
+
+

+ Supported on: + + + +

+

Set to + false + to disable HTTP compression for all responses.

+

+ Datatype: + bool +

+

+ Default: + true +

+
+
server.compression.referrerWhitelist + + +
+
+

+ Supported on: + + + +

+

Specifies an array of trusted hostnames, such as the {{kib}} host, or a reverse proxy sitting in front of it. This determines whether HTTP compression may be used for responses, based on the request + Referer + header. This setting may not be used when + + server.compression.enabled + + is set to + false.

+

+ Datatype: + string +

+
+
server.compression.brotli.enabled + + +
+
+

+ Supported on: + + + + +

+

Set to + true + to enable brotli (br) compression format. Browsers not supporting brotli compression will fallback to using gzip instead. This setting may not be used when + + server.compression.enabled + + is set to + false. It is available in {{ecloud}} 8.6.0 and later versions.

+

+ Datatype: + bool +

+

+ Default: + false +

+
+
+

Server CORS settings

+
+
server.cors.enabled + + + +
+
+

+ Supported on: + + + +

+

Set to + true + to allow cross-origin API calls.

+

+ Datatype: + bool +

+

+ Default: + false +

+
+
server.cors.allowCredentials + + + +
+
+

+ Supported on: + + + +

+

Set to + true + to allow browser code to access response body whenever request performed with user credentials.

+

+ Datatype: + bool +

+

+ Default: + false +

+
+
server.cors.allowOrigin + + + +
+
+

+ Supported on: + + + +

+

List of origins permitted to access resources. You must specify explicit hostnames and not use + server.cors.allowOrigin: ["*"] + when + server.cors.allowCredentials: true.

+

+ Datatype: + string +

+

+ Default: + ["*"] +

+
+
+

Server rate limiter settings

+
+
server.rateLimiter.enabled + + +
+
+

+ Supported on: + + + +

+

Enables rate-limiting of requests to the {{kib}} server based on Node.js' Event Loop Utilization. If the average event loop utilization for the specified term exceeds the configured threshold, the server will respond with a + 429 Too Many Requests + status code.

+

This functionality should be used carefully as it may impact the server's availability. The configuration options vary per environment, so it is recommended to enable this option in a testing environment first, adjust the rate-limiter configuration, and then roll it out to production.

+

+ Datatype: + bool +

+

+ Default: + false +

+
+
server.rateLimiter.elu + + +
+
+

+ Supported on: + + + +

+

The Event Loop Utilization (ELU) threshold for rate-limiting requests to the {{kib}} server. The ELU is a value between 0 and 1, representing the average event loop utilization over the specified term. If the average ELU exceeds this threshold, the server will respond with a + 429 Too Many Requests + status code.

+

In a multi-instance environment with autoscaling, this value is usually between 0.6 and 0.8 to give the autoscaler enough time to react. This value can be higher in a single-instance environment but should not exceed 1.0. In general, the lower the value, the more aggressive the rate limiting. And the highest possible option should be used to prevent the {{kib}} server from being terminated.

+

+ Datatype: + float +

+
+
server.rateLimiter.term + + +
+
+

+ Supported on: + + + +

+

This value is one of + short, + medium, or + long, representing the term over which the average event loop utilization is calculated. It uses exponential moving averages (EMA) to smooth out the utilization values. Each term corresponds to + 15s, + 30s, and + 60s, respectively.

+

The term value also changes the way the rate limiter sees the trend in the load:

+ +

This behavior prevents requests from being throttled if the load starts decreasing. In general, the shorter the term, the more aggressive the rate limiting. In the multi-instance environment, the + medium + term makes the most sense as it gives the {{kib}} server enough time to spin up a new instance and prevents the existing instances from being terminated.

+

+ Datatype: + enum +

+

+ Options: +

+ +
+
+

Server security response headers

+
+
server.securityResponseHeaders.strictTransportSecurity + + +
+
+

+ Supported on: + + + + +

+

Controls whether the + + Strict-Transport-Security + + header is used in all responses to the client from the {{kib}} server, and specifies what value is used. Allowed values are any text value or + null. To disable, set to + null.

+

+ Datatype: + string +

+

+ Default: + null +

+
+
server.securityResponseHeaders.xContentTypeOptions + + +
+
+

+ Supported on: + + + + +

+

Controls whether the + + X-Content-Type-Options + + header is used in all responses to the client from the {{kib}} server, and specifies what value is used. Allowed values are + nosniff + or + null. To disable, set to + null.

+

+ Datatype: + string +

+

+ Default: + "nosniff" +

+
+
server.securityResponseHeaders.referrerPolicy + + +
+
+

+ Supported on: + + + + +

+

Controls whether the + + Referrer-Policy + + header is used in all responses to the client from the {{kib}} server, and specifies what value is used. Allowed values are + no-referrer, + no-referrer-when-downgrade, + origin, + origin-when-cross-origin, + same-origin, + strict-origin, + strict-origin-when-cross-origin, + unsafe-url, or + null. To disable, set to + null.

+

+ Datatype: + string +

+

+ Default: + "strict-origin-when-cross-origin" +

+
+
server.securityResponseHeaders.permissionsPolicy + + + +
+
+

+ Supported on: + + + + +

+

Controls whether the + + Permissions-Policy + + header is used in all responses to the client from the {{kib}} server, and specifies what value is used. Allowed values are any text value or + null. Refer to the + + Permissions-Policy + documentation + for defined directives, values, and text format. To disable, set to + null.

+

+ Datatype: + string +

+

+ Default: + camera=(), display-capture=(), fullscreen=(self), geolocation=(), microphone=(), web-share=() +

+
+
server.securityResponseHeaders.permissionsPolicyReportOnly + + + +
+
+

+ Supported on: + + + + +

+

Controls whether the + + Permissions-Policy-Report-Only + + header is used in all responses to the client from the {{kib}} server, and specifies what value is used. Allowed values are any text value or + null. Refer to the + + Permissions-Policy + documentation + for defined directives, values, and text format.

+

+ Datatype: + string +

+
+
server.securityResponseHeaders.disableEmbedding + + +
+
+

+ Supported on: + + + + +

+

Controls whether the + + Content-Security-Policy + + and + + X-Frame-Options + + headers are configured to disable embedding {{kib}} in other webpages using iframes. When set to + true, secure headers are used to disable embedding, which adds the + frame-ancestors: 'self' + directive to the + Content-Security-Policy + response header and adds the + X-Frame-Options: SAMEORIGIN + response header.

+

+ Datatype: + bool +

+

+ Default: + false +

+
+
server.securityResponseHeaders.crossOriginOpenerPolicy + + +
+
+

+ Supported on: + + + + +

+

Controls whether the + + Cross-Origin-Opener-Policy + + header is used in all responses to the client from the {{kib}} server, and specifies what value is used. Allowed values are + unsafe-none, + same-origin-allow-popups, + same-origin, or + null. To disable, set to + null. It is available in {{ecloud}} 8.7.0 and later versions.

+

+ Datatype: + enum +

+

+ Default: + "same-origin" +

+

+ Options: +

+ +
+
+

Server SSL settings

+
+
server.ssl.certificate + + +
+
+

+ Supported on: + + + +

+

Path to a PEM-encoded X.509 server certificate. This is used by {{kib}} to establish trust when receiving inbound SSL/TLS connections from users.

+

+ Datatype: + string +

+
+
+ Note +
+
+

This setting cannot be used in conjunction with + + server.ssl.keystore.path + .

+
+
+
+
server.ssl.key + + +
+
+

+ Supported on: + + + +

+

Path to the corresponding private key for the PEM-encoded X.509 server certificate specified via + + server.ssl.certificate + . This is used by {{kib}} to establish trust when receiving inbound SSL/TLS connections from users.

+

+ Datatype: + string +

+
+
+ Note +
+
+

This setting cannot be used in conjunction with + + server.ssl.keystore.path + .

+
+
+
+
server.ssl.certificateAuthorities + + +
+
+

+ Supported on: + + + +

+

Paths to one or more PEM-encoded X.509 certificate authority (CA) certificates which make up a trusted certificate chain for {{kib}}. This chain is used by {{kib}} to establish trust when receiving inbound SSL/TLS connections from end users. If PKI authentication is enabled, this chain is also used by {{kib}} to verify client certificates from end users.

+

In addition to this setting, trusted certificates may be specified via + + server.ssl.keystore.path + + and/or + + server.ssl.truststore.path + .

+

+ Datatype: + string +

+
+
server.ssl.cipherSuites + + +
+
+

+ Supported on: + + + +

+

Details on the format, and the valid options, are available via the + OpenSSL cipher list format documentation.

+

+ Datatype: + string +

+

+ Default: + TLS_AES_256_GCM_SHA384 TLS_CHACHA20_POLY1305_SHA256 TLS_AES_128_GCM_SHA256 ECDHE-RSA-AES128-GCM-SHA256, ECDHE-ECDSA-AES128-GCM-SHA256, ECDHE-RSA-AES256-GCM-SHA384, ECDHE-ECDSA-AES256-GCM-SHA384, DHE-RSA-AES128-GCM-SHA256, ECDHE-RSA-AES128-SHA256, DHE-RSA-AES128-SHA256, ECDHE-RSA-AES256-SHA384, DHE-RSA-AES256-SHA384, ECDHE-RSA-AES256-SHA256, DHE-RSA-AES256-SHA256, HIGH,!aNULL, !eNULL, !EXPORT, !DES, !RC4, !MD5, !PSK, !SRP, !CAMELLIA +

+
+
server.ssl.clientAuthentication + + +
+
+

+ Supported on: + + + +

+

Controls the behavior in {{kib}} for requesting a certificate from client connections. Valid values are + "required", + "optional", and + "none". Using + "required" + will refuse to establish the connection unless a client presents a certificate, using + "optional" + will allow a client to present a certificate if it has one, and using + "none" + will prevent a client from presenting a certificate.

+

+ Datatype: + enum +

+

+ Default: + "none" +

+

+ Options: +

+ +
+
server.ssl.enabled + + +
+
+

+ Supported on: + + + +

+

Enables SSL/TLS for inbound connections to {{kib}}. When set to + true, a certificate and its corresponding private key must be provided. These can be specified via + + server.ssl.keystore.path + + or the combination of + + server.ssl.certificate + + and + + server.ssl.key + .

+

+ Datatype: + bool +

+

+ Default: + false +

+
+
server.ssl.keyPassphrase + + +
+
+

+ Supported on: + + + +

+

The password that decrypts the private key that is specified via + + server.ssl.key + . This value is optional, as the key may not be encrypted.

+

+ Datatype: + string +

+
+
server.ssl.keystore.path + + +
+
+

+ Supported on: + + + +

+

Path to a PKCS#12 keystore that contains an X.509 server certificate and its corresponding private key. If the keystore contains any additional certificates, those will be used as a trusted certificate chain for {{kib}}. All of these are used by {{kib}} to establish trust when receiving inbound SSL/TLS connections from end users. The certificate chain is also used by {{kib}} to verify client certificates from end users when PKI authentication is enabled.

+

In addition to this setting, trusted certificates may be specified via + + server.ssl.certificateAuthorities + + and/or + + server.ssl.truststore.path + .

+

+ Datatype: + string +

+
+
+ Note +
+
+

This setting cannot be used in conjunction with + + server.ssl.certificate + + or + + server.ssl.key + .

+
+
+
+
server.ssl.keystore.password + + +
+
+

+ Supported on: + + + +

+

The password that will be used to decrypt the keystore specified via + + server.ssl.keystore.path + . If the keystore has no password, leave this unset. If the keystore has an empty password, set this to + "".

+

+ Datatype: + string +

+
+
server.ssl.truststore.path + + +
+
+

+ Supported on: + + + +

+

Path to a PKCS#12 trust store that contains one or more X.509 certificate authority (CA) certificates which make up a trusted certificate chain for {{kib}}. This chain is used by {{kib}} to establish trust when receiving inbound SSL/TLS connections from end users. If PKI authentication is enabled, this chain is also used by {{kib}} to verify client certificates from end users.

+

In addition to this setting, trusted certificates may be specified via + + server.ssl.certificateAuthorities + + and/or + + server.ssl.keystore.path + .

+

+ Datatype: + string +

+
+
server.ssl.truststore.password + + +
+
+

+ Supported on: + + + +

+

The password that will be used to decrypt the trust store specified via + + server.ssl.truststore.path + . If the trust store has no password, leave this unset. If the trust store has an empty password, set this to + "".

+

+ Datatype: + string +

+
+
server.ssl.redirectHttpFromPort + + +
+
+

+ Supported on: + + + +

+

{{kib}} binds to this port and redirects all http requests to https over the port configured as + + server.port + .

+

+ Datatype: + int +

+
+
server.ssl.supportedProtocols + + +
+
+

+ Supported on: + + + +

+

An array of supported protocols with versions. Valid protocols: + TLSv1, + TLSv1.1, + TLSv1.2, + TLSv1.3. Enabling + TLSv1.1 + would require both setting the + --tls-min-1.1 + option in the + node.options + configuration and adding + TLSv1.1 + to + server.ssl.supportedProtocols. + HTTP/2 + requires the use of minimum + TLSv1.2 + for secure connections.

+

+ Datatype: + string +

+

+ Default: + TLSv1.2, TLSv1.3 +

+
+
+

Server XSRF settings

+
+
server.xsrf.allowlist + + +
+
+

+ Supported on: + + + + +

+

It is not recommended to disable protections for arbitrary API endpoints. Instead, supply the + kbn-xsrf + header. The + + server.xsrf.allowlist + + setting requires the following format:

+
+
+
+					*Default: [ ]* An array of API endpoints which should be exempt from Cross-Site Request Forgery ("XSRF") protections.
+					
+				
+
+
+

It is available in {{ecloud}} 8.0.0 and later versions.

+

+ Datatype: + string +

+

+ Default: + [] +

+
+
server.xsrf.disableProtection + + +
+
+

+ Supported on: + + + +

+

Setting this to + true + will completely disable Cross-site request forgery protection in Kibana. This is not recommended.

+

+ Datatype: + bool +

+

+ Default: + false +

+
+
+

Telemetry settings

+
+
telemetry.allowChangingOptInStatus + + +
+
+

+ Supported on: + + + +

+

When + false, users cannot change the opt-in status through + Advanced Settings, and {{kib}} only looks at the value of + + telemetry.optIn + + to determine whether to send telemetry data or not.

+

+ Datatype: + bool +

+

+ Default: + true +

+
+
telemetry.optIn + + +
+
+

+ Supported on: + + + +

+

Set to + false + to stop sending any telemetry data to Elastic. Reporting your cluster statistics helps us improve your user experience. When + false, the telemetry data is never sent to Elastic.

+

This setting can be changed at any time in + Advanced Settings. To prevent users from changing it, set + + telemetry.allowChangingOptInStatus + + to + false.

+

+ Datatype: + bool +

+

+ Default: + true +

+
+
+

UI and visualization settings

+
+
uiSettings.globalOverrides.hideAnnouncements + + + +
+
+

Set to + true + to stop showing messages and tours that highlight new features.

+

+ Datatype: + bool +

+

+ Default: + false +

+
+
uiSettings.globalOverrides.hideFeedback + + + +
+
+

Set to + true + to stop showing elements requesting user feedback.

+

+ Datatype: + bool +

+

+ Default: + false +

+
+
vis_type_table.legacyVisEnabled + + +
+
+

+ Supported on: + + + + +

+

Starting from version 7.11, a new datatable visualization is used. Set to + true + to enable the legacy version. In version 8.0 and later, the old implementation is removed and this setting is no longer supported.

+

+ Datatype: + bool +

+
+
vis_type_vega.enable + + +
+
+

+ Supported on: + + + + +

+

For 7.7 version and later, set to + false + to disable Vega vizualizations.

+

+ Datatype: + bool +

+

+ Default: + true +

+
+
vega.enableExternalUrls + + + +
+
+

+ Supported on: + + + + +

+

Set to + true + to allow Vega vizualizations to use data from sources other than the linked Elasticsearch cluster. In version 8.0 and later, the + vega.enableExternalUrls + is not supported. Use + vis_type_vega.enableExternalUrls + instead.

+

+ Datatype: + bool +

+
+
+ Deprecation details +
+
+

In version 8.0 and later, this setting is not supported. Use + vis_type_vega.enableExternalUrls + instead.

+
+
+
+
vis_type_vega.enableExternalUrls + + +
+
+

+ Supported on: + + + + +

+

Set this value to true to allow Vega to use any URL to access external data sources and images. When false, Vega can only get data from {{es}}.

+

+ Datatype: + bool +

+

+ Default: + false +

+
+
+

Feature and plugin settings

+
+
xpack.ccr.ui.enabled + + +
+
+

+ Supported on: + + + +

+

Set this value to false to disable the Cross-Cluster Replication UI.

+

+ Datatype: + bool +

+

+ Default: + true +

+
+
xpack.discoverEnhanced.actions.exploreDataInContextMenu.enabled + + +
+
+

+ Supported on: + + + +

+

Enables the + Explore underlying data + option that allows you to open + Discover + from a dashboard panel and view the panel data.

+

When you create visualizations using the + Lens + drag-and-drop editor, you can use the toolbar to open and explore your data in + Discover. For more information, check out + Explore the data in Discover.

+

+ Datatype: + bool +

+

+ Default: + false +

+
+
xpack.discoverEnhanced.actions.exploreDataInChart.enabled + + +
+
+

+ Supported on: + + + +

+

Enables you to view the underlying documents in a data series from a dashboard panel.

+

+ Datatype: + bool +

+

+ Default: + false +

+
+
xpack.ilm.ui.enabled + + +
+
+

+ Supported on: + + + +

+

Set this value to false to disable the Index Lifecycle Policies UI.

+

+ Datatype: + bool +

+

+ Default: + true +

+
+
xpack.index_management.ui.enabled + + +
+
+

+ Supported on: + + + +

+

Set this value to false to disable the Index Management UI.

+

+ Datatype: + bool +

+

+ Default: + true +

+
+
xpack.license_management.ui.enabled + + +
+
+

+ Supported on: + + + +

+

Set this value to false to disable the License Management UI.

+

+ Datatype: + bool +

+

+ Default: + true +

+
+
xpack.remote_clusters.ui.enabled + + +
+
+

+ Supported on: + + + +

+

Set this value to false to disable the Remote Clusters UI.

+

+ Datatype: + bool +

+

+ Default: + true +

+
+
xpack.rollup.ui.enabled + + + +
+
+

Set this value to false to disable the Rollup Jobs UI.

+

+ Datatype: + bool +

+

+ Default: + true +

+
+
+ Deprecation details +
+
+

Rollups are deprecated and will be removed in a future version. Use + downsampling + instead.

+
+
+
+
xpack.securitySolution.maxUploadResponseActionFileBytes + + +
+
+

+ Supported on: + + + + +

+

Allow to configure the max file upload size for use with the Upload File Response action available with the Defend Integration. To learn more, check + Endpoint Response actions. It is available in {{ecloud}} 8.9.0 and later versions.

+

+ Datatype: + string +

+
+
xpack.securitySolution.disableEndpointRuleAutoInstall + + + +
+
+

Set to + true + to disable the automatic installation of Elastic Defend SIEM rules when a new Endpoint integration policy is created. Introduced with v9.2.4.

+

+ Datatype: + bool +

+

+ Default: + false +

+
+
xpack.snapshot_restore.ui.enabled + + +
+
+

+ Supported on: + + + +

+

Set this value to false to disable the Snapshot and Restore UI.

+

+ Datatype: + bool +

+

+ Default: + true +

+
+
xpack.upgrade_assistant.ui.enabled + + +
+
+

+ Supported on: + + + +

+

Set this value to false to disable the Upgrade Assistant UI.

+

+ Datatype: + bool +

+

+ Default: + true +

+
+
diff --git a/tests/Elastic.Markdown.Tests/.snapshots/SettingsInclusion/IncludeTests/IncludesInclusionHtml.verified.html b/tests/Elastic.Markdown.Tests/.snapshots/SettingsInclusion/IncludeTests/IncludesInclusionHtml.verified.html new file mode 100644 index 0000000000..30e719857e --- /dev/null +++ b/tests/Elastic.Markdown.Tests/.snapshots/SettingsInclusion/IncludeTests/IncludesInclusionHtml.verified.html @@ -0,0 +1,1294 @@ +

General settings

+
+
xpack.encryptedSavedObjects.encryptionKey +
+
+

A string of 32 or more characters used to encrypt sensitive properties on alerting rules and actions before they're stored in Elasticsearch. Third party credentials — such as the username and password used to connect to an SMTP service — are an example of encrypted properties. Kibana offers a <<kibana-encryption-keys, CLI tool>> to help generate this encryption key. If not set, Kibana will generate a random key on startup, but all alerting and action functions will be blocked. Generated keys are not allowed for alerting and actions because when a new key is generated on restart, existing encrypted data becomes inaccessible. For the same reason, alerting and actions in high-availability deployments of Kibana will behave unexpectedly if the key isn't the same on all instances of Kibana. Although the key can be specified in clear text in + kibana.yml, it's recommended to store this key securely in the <<secure-settings,Kibana Keystore>>. Be sure to back up the encryption key value somewhere safe, as your alerting rules and actions will cease to function due to decryption failures should you lose it. If you want to rotate the encryption key, be sure to follow the instructions on <<encryption-key-rotation, encryption key rotation>>.

+
+
+

Action settings

+
+
xpack.actions.allowedHosts +
+
+

A list of hostnames that Kibana is allowed to connect to when built-in actions are triggered. It defaults to + ["*"], allowing any host, but keep in mind the potential for SSRF attacks when hosts are not explicitly added to the allowed hosts. An empty list + [] + can be used to block built-in actions from making any external connections. Note that hosts associated with built-in actions, such as Slack and PagerDuty, are not automatically added to allowed hosts. If you are not using the default + ["*"] + setting, you must ensure that the corresponding endpoints are added to the allowed hosts as well.

+
+
xpack.actions.customHostSettings +
+
+

A list of custom host settings to override existing global settings. Each entry in the list must have a + url + property, to associate a connection type (mail or https), hostname and port with the remaining options in the entry. The settings in + xpack.actions.customHostSettings + can be used to override the global option + xpack.actions.ssl.verificationMode + and provide customized TLS settings on a per-server basis. Set + xpack.actions.ssl.verificationMode + to the value to be used by default for all servers, then add an entry in + xpack.actions.customHostSettings + for every server that requires customized settings.

+

+ Default: + an empty list +

+

In the following example, two custom host settings are defined. The first provides a custom host setting for mail server + mail.example.com + using port 465 that supplies server certificate authentication data from both a file and inline, and requires TLS for the connection. The second provides a custom host setting for https server + webhook.example.com + which turns off server certificate authentication, that will allow Kibana to connect to the server if it's using a self-signed certificate. The individual properties that can be used in the settings are documented below.

+
+
+
+					xpack.actions.customHostSettings:   - url: smtp://mail.example.com:465     ssl:       verificationMode: 'full'       certificateAuthoritiesFiles: [ 'one.crt' ]       certificateAuthoritiesData: |           -----BEGIN CERTIFICATE-----           MIIDTD...           CwUAMD...           ... multiple lines of certificate data ...           -----END CERTIFICATE-----     smtp:       requireTLS: true   - url: https://webhook.example.com     ssl:       verificationMode: 'none'
+					
+				
+
+
+
+
xpack.actions.customHostSettings[n].url +
+
+

A URL associated with this custom host setting. Should be in the form of + protocol://hostname:port, where + protocol + is + https + or + smtp. If the port is not provided, 443 is used for + https + and 25 is used for + smtp. The + smtp + URLs are used for the Email actions that use this server, and the + https + URLs are used for actions which use + https + to connect to services. Entries with + https + URLs can use the + ssl + options, and entries with + smtp + URLs can use both the + ssl + and + smtp + options. No other URL values should be part of this URL, including paths, query strings, and authentication information. When an http or smtp request is made as part of running an action, only the protocol, hostname, and port of the URL for that request are used to look up these configuration values.

+
+
xpack.actions.customHostSettings[n].smtp.ignoreTLS +
+
+

A boolean value indicating that TLS must not be used for this connection. The options + smtp.ignoreTLS + and + smtp.requireTLS + can not both be set to true.

+

+ Default: + false +

+
+
xpack.actions.customHostSettings[n].smtp.requireTLS +
+
+

A boolean value indicating that TLS must be used for this connection. The options + smtp.ignoreTLS + and + smtp.requireTLS + can not both be set to true.

+

+ Default: + false +

+
+
xpack.actions.customHostSettings[n].ssl.verificationMode +
+
+

Controls the verification of the server certificate that Kibana receives when making an outbound SSL/TLS connection to the host server. Valid values are + full, + certificate, and + none. Use + full + to perform hostname verification, + certificate + to skip hostname verification, and + none + to skip verification. Default: + full. <<elasticsearch-ssl-verificationMode,Equivalent Kibana setting>>. Overrides the general + xpack.actions.ssl.verificationMode + configuration for requests made for this hostname/port.

+
+
xpack.actions.customHostSettings[n].ssl.certificateAuthoritiesFiles +
+
+

A file name or list of file names of PEM-encoded certificate files to use to validate the server.

+
+
xpack.actions.customHostSettings[n].ssl.certificateAuthoritiesData +
+
+

The contents of one or more PEM-encoded certificate files in multiline format. This configuration can be used for environments where the files cannot be made available.

+
+
xpack.actions.email.domain_allowlist +
+
+

A list of allowed email domains which can be used with the email connector. When this setting is not used, all email domains are allowed. When this setting is used, if any email is attempted to be sent that (a) includes an addressee with an email domain that is not in the allowlist, or (b) includes a from address domain that is not in the allowlist, it will fail with a message indicating the email is not allowed.

+
+
+ Warning +
+
+

This feature is available in Kibana 7.17.4 and 8.3.0 onwards but is not supported in Kibana 8.0, 8.1 or 8.2. As such, this setting should be removed before upgrading from 7.17 to 8.0, 8.1 or 8.2. It is possible to configure the settings in 7.17.4 and then upgrade to 8.3.0 directly.

+
+
+
+
xpack.actions.enableFooterInEmail +
+
+

A boolean value indicating that a footer with a relevant link should be added to emails sent as alerting actions.

+

+ Default: + true +

+
+
xpack.actions.enabledActionTypes +
+
+ +
+
+ Important +
+
+

<<pre-configured-connectors,Preconfigured connectors>> are not affected by this setting.

+
+
+
+
xpack.actions.microsoftExchangeUrl +
+
+

The URL for the Microsoft Azure Active Directory endpoint to use for MS Exchange email authentication.

+

+ Default: + https://login.microsoftonline.com +

+
+
xpack.actions.microsoftGraphApiUrl +
+
+

The URL for the Microsoft Graph API endpoint to use for MS Exchange email authentication.

+

+ Default: + https://graph.microsoft.com/v1.0 +

+
+
xpack.actions.microsoftGraphApiScope +
+
+

The URL for the Microsoft Graph API scope endpoint to use for MS Exchange email authentication.

+

+ Default: + https://graph.microsoft.com/.default +

+
+
xpack.actions.proxyUrl +
+
+

Specifies the proxy URL to use, if using a proxy for actions. By default, no proxy is used.

+ +

To help diagnose problems using a proxy, you can use the + curl + command with options to use your proxy, and log debug information, with the following command, replacing the proxy and target URLs as appropriate. This will force the request to be made to the proxy in tunneling mode, and display some of the interaction between the client and the proxy.

+
+
+
+					curl --verbose --proxytunnel --proxy http://localhost:8080 http://example.com
+					
+				
+
+
+
+
xpack.actions.proxyBypassHosts +
+
+

Specifies hostnames which should not use the proxy, if using a proxy for actions. The value is an array of hostnames as strings. By default, all hosts will use the proxy, but if an action's hostname is in this list, the proxy will not be used. The settings + xpack.actions.proxyBypassHosts + and + xpack.actions.proxyOnlyHosts + cannot be used at the same time.

+

For example:

+
+
+
+					xpack.actions.proxyBypassHosts: [ "events.pagerduty.com" ]
+					
+				
+
+
+

If applicable, include the subdomain in the hostname

+
+
xpack.actions.proxyOnlyHosts +
+
+

Specifies hostnames which should only use the proxy, if using a proxy for actions. The value is an array of hostnames as strings. By default, no hosts will use the proxy, but if an action's hostname is in this list, the proxy will be used. The settings + xpack.actions.proxyBypassHosts + and + xpack.actions.proxyOnlyHosts + cannot be used at the same time.

+

For example:

+
+
+
+					xpack.actions.proxyOnlyHosts: [ "events.pagerduty.com" ]
+					
+				
+
+
+

If applicable, include the subdomain in the hostname

+
+
xpack.actions.proxyHeaders +
+
+

Specifies HTTP headers for the proxy, if using a proxy for actions.

+

+ Default: + {} +

+
+
xpack.actions.ssl.proxyVerificationMode +
+
+

Controls the verification for the proxy server certificate that Kibana receives when making an outbound SSL/TLS connection to the proxy server. Use + full + to perform hostname verification, + certificate + to skip hostname verification, and + none + to skip verification. <<elasticsearch-ssl-verificationMode,Equivalent Kibana setting>>

+

+ Default: + full +

+

+ Options: +

+ +
+
xpack.actions.ssl.verificationMode +
+
+

Controls the verification for the server certificate that Elastic Maps Server receives when making an outbound SSL/TLS connection for actions. Valid values are + full, + certificate, and + none. Use + full + to perform hostname verification, + certificate + to skip hostname verification, and + none + to skip verification. <<elasticsearch-ssl-verificationMode,Equivalent Kibana setting>> This setting can be overridden for specific URLs by using the setting + xpack.actions.customHostSettings[n].ssl.verificationMode + (described above) to a different value.

+

+ Default: + full +

+

+ Options: +

+ +
+
xpack.actions.maxResponseContentLength +
+
+

Specifies the max number of bytes of the http response for requests to external resources.

+

+ Default: + 1000000 (1MB) +

+
+
xpack.actions.responseTimeout +
+
+

Specifies the time allowed for requests to external resources. Requests that take longer are canceled. The time is formatted as a number and a time unit (ms, + s, + m, + h, + d, + w, + M, or + Y). For example, + 20m, + 24h, + 7d, + 1w. Default: + 60s.

+
+
xpack.actions.run.maxAttempts +
+
+

Specifies the maximum number of times an action can be attempted to run.

+

+ Options: +

+ +
+
xpack.actions.run.connectorTypeOverrides +
+
+

Overrides the configs under + xpack.actions.run + for the connector type with the given ID. List the connector type identifier and its settings in an array of objects.

+

For example:

+
+
+
+					xpack.actions.run:     maxAttempts: 1     connectorTypeOverrides:         - id: '.server-log'           maxAttempts: 5
+					
+				
+
+
+
+
xpack.actions.queued.max +
+
+

Specifies the maximum number of actions that can be queued.

+

+ Default: + 1000000 +

+
+
+

Preconfigured connector settings

+

These settings vary depending on which type of preconfigured connector you're adding.

+

For example:

+
+

+ [source,yaml] +

+
+
+

+ xpack.actions.preconfigured: my-server-log: name: preconfigured-server-log-connector-type actionTypeId: .server-log +

+
+

For more examples, go to <<pre-configured-connectors>>.

+
+
xpack.actions.preconfiguredAlertHistoryEsIndex +
+
+

Enables a preconfigured alert history Elasticsearch <<index-action-type, Index>> connector.

+

+ Default: + false +

+
+
xpack.actions.preconfigured +
+
+

Specifies configuration details that are specific to the type of preconfigured connector.

+
+
xpack.actions.preconfigured.<connector-id>.actionTypeId +
+
+

The type of preconfigured connector.

+

+ Options: +

+ +
+
xpack.actions.preconfigured.<connector-id>.config +
+
+

The configuration details, which are specific to the type of preconfigured connector.

+
+
xpack.actions.preconfigured.<connector-id>.config.apiProvider +
+
+

For a <<openai-action-type,OpenAI connector>>, specifies the OpenAI API provider.

+

+ Options: +

+ +
+
xpack.actions.preconfigured.<connector-id>.config.apiUrl +
+
+

A configuration URL that varies by connector:

+ +
+
+ Note +
+
+

If you are using the + xpack.actions.allowedHosts + setting, make sure the hostname in the URL is added to the allowed hosts.

+
+
+
+
xpack.actions.preconfigured.<connector-id>.config.appId +
+
+

An application ID that varies by connector:

+ +
+
xpack.actions.preconfigured.<connector-id>.config.clientId +
+
+

A client identifier that varies by connector:

+ +
+
xpack.actions.preconfigured.<connector-id>.config.configUrl +
+
+

For an <<xmatters-action-type,xMatters connector>> with basic authentication, specifies the request URL for the Elastic Alerts trigger in xMatters.

+
+
xpack.actions.preconfigured.<connector-id>.config.createCommentJson +
+
+

For a <<cases-webhook-action-type,{webhook-cm} connector>>, specifies a stringified JSON payload with Mustache variables that is sent to the create comment URL to create a case comment. The required variable is + case.description.

+
+
+ Note +
+
+

The JSON is validated after the Mustache variables have been placed when the REST method runs. You should manually ensure that the JSON is valid, disregarding the Mustache variables, so the later validation will pass.

+
+
+
+
xpack.actions.preconfigured.<connector-id>.config.createCommentMethod +
+
+

For a <<cases-webhook-action-type,{webhook-cm} connector>>, specifies the REST API HTTP request method to create a case comment in the third-party system.

+

+ Default: + put +

+

+ Options: +

+ +
+
xpack.actions.preconfigured.<connector-id>.config.createCommentUrl +
+
+

For a <<cases-webhook-action-type,{webhook-cm} connector>>, specifies a REST API URL string to create a case comment by ID in the third-party system.

+
+
+ Note +
+
+

If you are using the + xpack.actions.allowedHosts + setting, make sure the hostname in the URL is added to the allowed hosts.

+
+
+
+
xpack.actions.preconfigured.<connector-id>.config.createIncidentJson +
+
+

For a <<cases-webhook-action-type,{webhook-cm} connector>>, specifies a stringified JSON payload with Mustache variables that is sent to the create case URL to create a case. Required variables are + case.title + and + case.description.

+
+
+ Note +
+
+

The JSON is validated after the Mustache variables have been placed when the REST method runs. You should manually ensure that the JSON is valid, disregarding the Mustache variables, so the later validation will pass.

+
+
+
+
xpack.actions.preconfigured.<connector-id>.config.createIncidentMethod +
+
+

For a <<cases-webhook-action-type,{webhook-cm} connector>>, specifies the REST API HTTP request method to create a case in the third-party system

+

+ Default: + post +

+

+ Options: +

+ +
+
xpack.actions.preconfigured.<connector-id>.config.createIncidentUrl +
+
+

For a <<cases-webhook-action-type,{webhook-cm} connector>>, specifies a REST API URL string to create a case in the third-party system.

+
+
+ Note +
+
+

If you are using the + xpack.actions.allowedHosts + setting, make sure the hostname in the URL is added to the allowed hosts.

+
+
+
+
xpack.actions.preconfigured.<connector-id>.config.createIncidentResponseKey +
+
+

For a <<cases-webhook-action-type,{webhook-cm} connector>>, specifies a string from the response body of the create case method that corresponds to the external service identifier.

+
+
xpack.actions.preconfigured.<connector-id>.config.defaultModel +
+
+

The default model to use for requests, which varies by connector:

+ +
+
xpack.actions.preconfigured.<connector-id>.config.executionTimeField +
+
+

For an <<index-action-type,index connector>>, a field that indicates when the document was indexed.

+
+
xpack.actions.preconfigured.<connector-id>.config.from +
+
+

For an <<email-action-type,email connector>>, specifies the from address for all emails sent by the connector. It must be specified in + user@host-name + format.

+
+
xpack.actions.preconfigured.<connector-id>.config.getIncidentResponseExternalTitleKey +
+
+

For a <<cases-webhook-action-type,{webhook-cm} connector>>, specifies a string from the response body of the get case method that corresponds to the external service title.

+
+
xpack.actions.preconfigured.<connector-id>.config.getIncidentUrl +
+
+

For a <<cases-webhook-action-type,{webhook-cm} connector>>, specifies a REST API URL string with an external service ID Mustache variable to get the case from the third-party system.

+
+
+ Note +
+
+

If you are using the + xpack.actions.allowedHosts + setting, make sure the hostname in the URL is added to the allowed hosts.

+
+
+
+
xpack.actions.preconfigured.<connector-id>.config.hasAuth +
+
+

For an <<email-action-type,email>>, <<webhook-action-type,webhook>>, or <<cases-webhook-action-type,{webhook-cm} connector>>, specifies whether a user and password are required inside the secrets configuration.

+

+ Default: + true +

+
+
xpack.actions.preconfigured.<connector-id>.config.headers +
+
+

For a <<webhook-action-type,webhook>> or <<cases-webhook-action-type,{webhook-cm} connector>>, specifies a set of key-value pairs sent as headers with the request.

+
+
xpack.actions.preconfigured.<connector-id>.config.host +
+
+

For an <<email-action-type,email connector>>, specifies the host name of the service provider.

+
+
xpack.actions.preconfigured.<connector-id>.config.index +
+
+

For an <<index-action-type,index connector>>, specifies the Elasticsearch index.

+
+
xpack.actions.preconfigured.<connector-id>.config.isOAuth +
+
+

For a <<servicenow-action-type,{sn-itsm}>>, <<servicenow-sir-action-type,{sn-sir}>>, or <<servicenow-itom-action-type,{sn-itom} connector>>, specifies whether to use basic or OAuth authentication.

+
+
xpack.actions.preconfigured.<connector-id>.config.jwtKeyId +
+
+

For a <<servicenow-action-type,{sn-itsm}>>, <<servicenow-sir-action-type,{sn-sir}>>, or <<servicenow-itom-action-type,{sn-itom} connector>>, specifies the key ID assigned to the JWT verifier map of your OAuth application. It is required when + xpack.actions.preconfigured.<connector-id>.config.isOAuth + is + true.

+
+
xpack.actions.preconfigured.<connector-id>.config.mappings +
+
+

For a <<swimlane-action-type,Swimlane connector>>, specifies field mappings.

+
+
xpack.actions.preconfigured.<connector-id>.config.mappings.alertIdConfig +
+
+

For a <<swimlane-action-type,Swimlane connector>>, field mapping for the alert identifier. You must provide + fieldtype, + id, + key, and + name + values.

+
+
xpack.actions.preconfigured.<connector-id>.config.mappings.caseIdConfig +
+
+

For a <<swimlane-action-type,Swimlane connector>>, field mapping for the case identifier. You must provide + fieldtype, + id, + key, and + name + values.

+
+
xpack.actions.preconfigured.<connector-id>.config.mappings.caseNameConfig +
+
+

For a <<swimlane-action-type,Swimlane connector>>, field mapping for the case name. You must provide + fieldtype, + id, + key, and + name + values.

+
+
xpack.actions.preconfigured.<connector-id>.config.mappings.commentsConfig +
+
+

For a <<swimlane-action-type,Swimlane connector>>, field mapping for the case comments. You must provide + fieldtype, + id, + key, and + name + values.

+
+
xpack.actions.preconfigured.<connector-id>.config.mappings.descriptionConfig +
+
+

For a <<swimlane-action-type,Swimlane connector>>, field mapping for the case description. You must provide + fieldtype, + id, + key, and + name + values.

+
+
xpack.actions.preconfigured.<connector-id>.config.mappings.ruleNameConfig +
+
+

For a <<swimlane-action-type,Swimlane connector>>, field mapping for the rule name. You must provide + fieldtype, + id, + key, and + name + values.

+
+
xpack.actions.preconfigured.<connector-id>.config.mappings.severityConfig +
+
+

For a <<swimlane-action-type,Swimlane connector>>, specifies a field mapping for the severity. You must provide + fieldtype, + id, + key, and + name + values.

+
+
xpack.actions.preconfigured.<connector-id>.config.method +
+
+

For a <<webhook-action-type,webhook connector>>, specifies the HTTP request method, either + post + or + put. Defaults to + post.

+
+
xpack.actions.preconfigured.<connector-id>.config.orgId +
+
+

For an <<resilient-action-type,{ibm-r} connector>>, specifies the {ibm-r} organization identifier.

+
+
xpack.actions.preconfigured.<connector-id>.config.port +
+
+

For an <<email-action-type,email connector>>, specifies the port to connect to on the service provider.

+
+
xpack.actions.preconfigured.<connector-id>.config.projectKey +
+
+

For a <<jira-action-type,Jira connector>>, specifies the Jira project key.

+
+
xpack.actions.preconfigured.<connector-id>.config.secure +
+
+

For an <<email-action-type,email connector>>, specifies whether the connection will use TLS when connecting to the service provider. If not true, the connection will initially connect over TCP then attempt to switch to TLS via the SMTP STARTTLS command.

+
+
xpack.actions.preconfigured.<connector-id>.config.service +
+
+

For an <<email-action-type,email connector>>, specifies the name of the email service. For example, + elastic_cloud, + exchange_server, + gmail, + other, + outlook365, or + ses.

+
+
xpack.actions.preconfigured.<connector-id>.config.tenantId +
+
+

For an <<email-action-type,email connector>>, specifies a GUID format value that corresponds to a tenant ID, which is a part of OAuth 2.0 client credentials authentication.

+
+
xpack.actions.preconfigured.<connector-id>.config.updateIncidentJson +
+
+

For a <<cases-webhook-action-type,{webhook-cm} connector>>, specifies a stringified JSON payload with Mustache variables that is sent to the update case URL to update a case. Required variables are + case.title + and + case.description.

+
+
+ Note +
+
+

The JSON is validated after the Mustache variables have been placed when the REST method runs. You should manually ensure that the JSON is valid, disregarding the Mustache variables, so the later validation will pass.

+
+
+
+
xpack.actions.preconfigured.<connector-id>.config.updateIncidentMethod +
+
+

For a <<cases-webhook-action-type,{webhook-cm} connector>>, specifies the REST API HTTP request method to update the case in the third-party system.

+

+ Default: + put +

+

+ Options: +

+ +
+
xpack.actions.preconfigured.<connector-id>.config.updateIncidentUrl +
+
+

For a <<cases-webhook-action-type,{webhook-cm} connector>>, specifies the REST API URL to update the case by ID in the third-party system.

+
+
+ Note +
+
+

If you are using the + xpack.actions.allowedHosts + setting, make sure the hostname in the URL is added to the allowed hosts.

+
+
+
+
xpack.actions.preconfigured.<connector-id>.config.url +
+
+

A configuration URL that varies by connector:

+ +
+
+ Note +
+
+

If you are using the + xpack.actions.allowedHosts + setting, make sure this hostname is added to the allowed hosts.

+
+
+
+
xpack.actions.preconfigured.<connector-id>.config.userIdentifierValue +
+
+

For a <<servicenow-action-type,{sn-itsm}>>, <<servicenow-sir-action-type,{sn-sir}>>, or <<servicenow-itom-action-type,{sn-itom} connector>>, specifies the user identifier. It is required when required when + xpack.actions.preconfigured.<connector-id>.config.isOAuth + is + true.

+
+
xpack.actions.preconfigured.<connector-id>.config.usesBasic +
+
+

For an <<xmatters-action-type,xMatters connector>>, specifies whether it uses HTTP basic authentication.

+

+ Default: + true +

+
+
xpack.actions.preconfigured.<connector-id>.config.usesTableApi +
+
+

For a <<servicenow-action-type,{sn-itsm}>> or <<servicenow-sir-action-type,{sn-sir} connector>>, specifies whether the connector uses the Table API or the Import Set API. If set to + false, the Elastic application should be installed in ServiceNow.

+
+
xpack.actions.preconfigured.<connector-id>.config.viewIncidentUrl +
+
+

For a <<cases-webhook-action-type,{webhook-cm} connector>>, specifies a URL string with either the external service ID or external service title Mustache variable to view a case in the external system.

+
+
xpack.actions.preconfigured.<connector-id>.config.webhookIntegrationUrl +
+
+

For a <<torq-action-type,Torq connector>>, specifies the endpoint URL of the Elastic Security integration in Torq.

+
+
xpack.actions.preconfigured.<connector-id>.name +
+
+

The name of the preconfigured connector.

+
+
xpack.actions.preconfigured.<connector-id>.secrets +
+
+

Sensitive configuration details, such as username, password, and keys, which are specific to the connector type.

+
+
+ Tip +
+
+

Sensitive properties, such as passwords, should be stored in the <<creating-keystore,Kibana keystore>>.

+
+
+
+
xpack.actions.preconfigured.<connector-id>.secrets.accessKey +
+
+

For an <<bedrock-action-type,{bedrock} connector>>, specifies the AWS access key for authentication.

+
+
xpack.actions.preconfigured.<connector-id>.secrets.apikey +
+
+

An API key secret that varies by connector.

+
+
xpack.actions.preconfigured.<connector-id>.secrets.credentialsJson +
+
+

For an <<gemini-action-type,{gemini} connector>>, specifies the GCP service account credentials JSON file for authentication.

+ +
+
xpack.actions.preconfigured.<connector-id>.secrets.apiKeyId +
+
+

For an <<resilient-action-type,{ibm-r} connector>>, specifies the authentication key ID for HTTP basic authentication.

+
+
xpack.actions.preconfigured.<connector-id>.secrets.apiKeySecret +
+
+

For an <<resilient-action-type,{ibm-r} connector>>, specifies the authentication key secret for HTTP basic authentication.

+
+
xpack.actions.preconfigured.<connector-id>.secrets.apiToken +
+
+

For a <<jira-action-type,Jira>> or <<swimlane-action-type,{swimlane} connector>>, specifies the API authentication token for HTTP basic authentication.

+
+
xpack.actions.preconfigured.<connector-id>.secrets.clientSecret +
+
+

A client secret that varies by connector:

+ +
+
+ Note +
+
+

The client secret must be URL-encoded.

+
+
+
+
xpack.actions.preconfigured.<connector-id>.secrets.email +
+
+

An email address that varies by connector:

+ +
+
xpack.actions.preconfigured.<connector-id>.secrets.password +
+
+

A password secret that varies by connector:

+ +
+
xpack.actions.preconfigured.<connector-id>.secrets.privateKey +
+
+

For a <<servicenow-action-type,{sn-itsm}>>, <<servicenow-sir-action-type,{sn-sir}>>, or <<servicenow-itom-action-type,{sn-itom} connector>>, specifies the RSA private key. It is required when + xpack.actions.preconfigured.<connector-id>.config.isOAuth + is + true.

+
+
xpack.actions.preconfigured.<connector-id>.secrets.privateKeyPassword +
+
+

For a <<servicenow-action-type,{sn-itsm}>>, <<servicenow-sir-action-type,{sn-sir}>>, or <<servicenow-itom-action-type,{sn-itom} connector>>, specifies the password for the RSA private key.

+
+
xpack.actions.preconfigured.<connector-id>.secrets.routingKey +
+
+

For a <<pagerduty-action-type,PagerDuty connector>>, specifies the 32 character PagerDuty Integration Key for an integration on a service, also referred to as the routing key.

+
+
xpack.actions.preconfigured.<connector-id>.secrets.secret +
+
+

For an <<bedrock-action-type,{bedrock} connector>>, specifies the AWS secret for authentication.

+
+
xpack.actions.preconfigured.<connector-id>.secrets.secretsUrl +
+
+

For an <<xmatters-action-type,xMatters connector>> with URL authentication, specifies the request URL for the Elastic Alerts trigger in xMatters with the API key included in the URL. It is used only when + xpack.actions.preconfigured.<connector-id>.config.usesBasic + is + false.

+
+
+ Note +
+
+

If you are using the + xpack.actions.allowedHosts + setting, make sure this hostname is added to the allowed hosts.

+
+
+
+
xpack.actions.preconfigured.<connector-id>.secrets.token +
+
+

A token secret that varies by connector:

+ +
+
xpack.actions.preconfigured.<connector-id>.secrets.user +
+
+

A user name secret that varies by connector:

+ +
+
xpack.actions.preconfigured.<connector-id>.secrets.webhookUrl +
+
+

A URL that varies by connector:

+ +
+
+ Note +
+
+

If you are using the + xpack.actions.allowedHosts + setting, make sure the hostname is added to the allowed hosts.

+
+
+
+
xpack.actions.preconfigured.<connector-id>.secrets.username +
+
+

For a <<servicenow-action-type,{sn-itsm}>>, <<servicenow-sir-action-type,{sn-sir}>>, or <<servicenow-itom-action-type,{sn-itom} connector>>, specifies a user name that is required when + xpack.actions.preconfigured.<connector-id>.config.isOAuth + is + false.

+
+
+

Alerting settings

+
+
xpack.alerting.cancelAlertsOnRuleTimeout +
+
+

Specifies whether to skip writing alerts and scheduling actions if rule processing was cancelled due to a timeout. This setting can be overridden by individual rule types.

+

+ Default: + true +

+
+
xpack.alerting.rules.maxScheduledPerMinute +
+
+

Specifies the maximum number of rules to run per minute.

+

+ Default: + 10000 +

+
+
xpack.alerting.rules.minimumScheduleInterval.value +
+
+

Specifies the minimum schedule interval for rules. This minimum is applied to all rules created or updated after you set this value. The time is formatted as a number and a time unit (s, + m, + h, or + d). For example, + 20m, + 24h, + 7d. This duration cannot exceed + 1d.

+

+ Default: + 1m +

+
+
xpack.alerting.rules.minimumScheduleInterval.enforce +
+
+

Specifies the behavior when a new or changed rule has a schedule interval less than the value defined in + xpack.alerting.rules.minimumScheduleInterval.value. If + false, rules with schedules less than the interval will be created but warnings will be logged. If + true, rules with schedules less than the interval cannot be created.

+

+ Default: + false +

+
+
xpack.alerting.rules.run.actions.max +
+
+

Specifies the maximum number of actions that a rule can generate each time detection checks run.

+
+
xpack.alerting.rules.run.alerts.max +
+
+

Specifies the maximum number of alerts that a rule can generate each time detection checks run.

+

+ Default: + 1000 +

+
+
+ Warning +
+
+

The exact number of alerts your cluster can safely handle depends on your cluster configuration and workload, however setting a value higher than the default (1000) is not recommended or supported. Doing so could strain system resources and lead to performance issues, delays in alert processing, and potential disruptions during high alert activity periods.

+
+
+
+
xpack.alerting.rules.run.timeout +
+
+

Specifies the default timeout for tasks associated with all types of rules. The time is formatted as a number and a time unit (ms, + s, + m, + h, + d, + w, + M, or + Y). For example, + 20m, + 24h, + 7d, + 1w. Default: + 5m.

+
+
xpack.alerting.rules.run.ruleTypeOverrides +
+
+

Overrides the configs under + xpack.alerting.rules.run + for the rule type with the given ID. List the rule identifier and its settings in an array of objects.

+

For example:

+
+
+
+					xpack.alerting.rules.run:     timeout: '5m'     ruleTypeOverrides:         - id: '.index-threshold'           timeout: '15m'
+					
+				
+
+
+
+
xpack.alerting.rules.run.actions.connectorTypeOverrides +
+
+

Overrides the configs under + xpack.alerting.rules.run.actions + for the connector type with the given ID. List the connector type identifier and its settings in an array of objects.

+

For example:

+
+
+
+					xpack.alerting.rules.run:     actions:         max: 10         connectorTypeOverrides:             - id: '.server-log'               max: 5
+					
+				
+
+
+
+
diff --git a/tests/Elastic.Markdown.Tests/.snapshots/SettingsInclusion/NewSchemaRendersMetadataAndNestedSettings/GeneratesHtml.verified.html b/tests/Elastic.Markdown.Tests/.snapshots/SettingsInclusion/NewSchemaRendersMetadataAndNestedSettings/GeneratesHtml.verified.html new file mode 100644 index 0000000000..4de6cc05ef --- /dev/null +++ b/tests/Elastic.Markdown.Tests/.snapshots/SettingsInclusion/NewSchemaRendersMetadataAndNestedSettings/GeneratesHtml.verified.html @@ -0,0 +1,54 @@ +

General settings

+
+
xpack.actions.customHostSettings + + + +
+
+

+ Supported on: + + + +

+

Parent setting description.

+

+ Datatype: + object +

+

+ Default: + [] +

+

+ Options: +

+ +
+
xpack.actions.customHostSettings[n].url + + + +
+
+

+ Supported on: + + + +

+

Child setting description.

+

+ Datatype: + string +

+
+
+
+
diff --git a/tests/Elastic.Markdown.Tests/.snapshots/SettingsInclusion/SettingReusesAnAnchoredAppliesToBlock/RendersBothSettings.verified.html b/tests/Elastic.Markdown.Tests/.snapshots/SettingsInclusion/SettingReusesAnAnchoredAppliesToBlock/RendersBothSettings.verified.html new file mode 100644 index 0000000000..5aabb8364d --- /dev/null +++ b/tests/Elastic.Markdown.Tests/.snapshots/SettingsInclusion/SettingReusesAnAnchoredAppliesToBlock/RendersBothSettings.verified.html @@ -0,0 +1,31 @@ +

Example

+
+
xpack.sample.one + + + +
+
+

+ Supported on: + + + +

+

First setting.

+
+
xpack.sample.two + + + +
+
+

+ Supported on: + + + +

+

Second setting, reuses the same applies_to block as the one above.

+
+
diff --git a/tests/Elastic.Markdown.Tests/.snapshots/SettingsInclusion/SettingsTopMatterAndTitlesRender/RendersPageDescriptionNotesAndInterpolatedGroupTitle.verified.html b/tests/Elastic.Markdown.Tests/.snapshots/SettingsInclusion/SettingsTopMatterAndTitlesRender/RendersPageDescriptionNotesAndInterpolatedGroupTitle.verified.html new file mode 100644 index 0000000000..e975475abc --- /dev/null +++ b/tests/Elastic.Markdown.Tests/.snapshots/SettingsInclusion/SettingsTopMatterAndTitlesRender/RendersPageDescriptionNotesAndInterpolatedGroupTitle.verified.html @@ -0,0 +1,27 @@ +

Read the + preconfigured connectors + guide.

+
+
+ Note +
+
+

Top-level note for Kibana.

+
+
+

General Kibana settings

+
+
+ Note +
+
+

Group-level note for Kibana.

+
+
+
+
xpack.sample.enabled +
+
+

Enables sample behavior.

+
+
diff --git a/tests/Elastic.Markdown.Tests/AppliesTo/ApplicableToJsonConverterSerializationTests.cs b/tests/Elastic.Markdown.Tests/AppliesTo/ApplicableToJsonConverterSerializationTests.cs index 2b0967e78c..85ac54c242 100644 --- a/tests/Elastic.Markdown.Tests/AppliesTo/ApplicableToJsonConverterSerializationTests.cs +++ b/tests/Elastic.Markdown.Tests/AppliesTo/ApplicableToJsonConverterSerializationTests.cs @@ -8,6 +8,7 @@ using Elastic.Documentation; using Elastic.Documentation.AppliesTo; using Elastic.Documentation.Versions; +using Nullean.Snapshots.Json; namespace Elastic.Markdown.Tests.AppliesTo; @@ -23,7 +24,7 @@ public void SerializeStackProducesCorrectJson() var json = JsonSerializer.Serialize(applicableTo, _options); // language=json - json.Should().Be( + json.Should().MatchInlineJsonSnapshot( """ [ { @@ -48,7 +49,7 @@ public void SerializeStackWithVersionProducesCorrectJson() var json = JsonSerializer.Serialize(applicableTo, _options); // language=json - json.Should().Be( + json.Should().MatchInlineJsonSnapshot( """ [ { @@ -76,7 +77,7 @@ public void SerializeMultipleApplicabilitiesProducesCorrectJson() var json = JsonSerializer.Serialize(applicableTo, _options); // language=json - json.Should().Be( + json.Should().MatchInlineJsonSnapshot( """ [ { @@ -113,7 +114,7 @@ public void SerializeDeploymentProducesCorrectJson() var json = JsonSerializer.Serialize(applicableTo, _options); // language=json - json.Should().Be( + json.Should().MatchInlineJsonSnapshot( """ [ { @@ -150,7 +151,7 @@ public void SerializeServerlessProducesCorrectJson() var json = JsonSerializer.Serialize(applicableTo, _options); // language=json - json.Should().Be( + json.Should().MatchInlineJsonSnapshot( """ [ { @@ -181,7 +182,7 @@ public void Serialize_ServerlessVectorDatabase_ProducesCorrectJson() var json = JsonSerializer.Serialize(applicableTo, _options); // language=json - json.Should().Be( + json.Should().MatchInlineJsonSnapshot( """ [ { @@ -208,7 +209,7 @@ public void SerializeProductProducesCorrectJson() var json = JsonSerializer.Serialize(applicableTo, _options); // language=json - json.Should().Be( + json.Should().MatchInlineJsonSnapshot( """ [ { @@ -239,7 +240,7 @@ public void SerializeProductApplicabilityProducesCorrectJson() var json = JsonSerializer.Serialize(applicableTo, _options); // language=json - json.Should().Be( + json.Should().MatchInlineJsonSnapshot( """ [ { @@ -276,12 +277,48 @@ public void SerializeAllLifecyclesProducesCorrectJson() var json = JsonSerializer.Serialize(applicableTo, _options); - json.Should().Contain("\"lifecycle\": \"preview\""); - json.Should().Contain("\"lifecycle\": \"experimental\""); - json.Should().Contain("\"lifecycle\": \"beta\""); - json.Should().Contain("\"lifecycle\": \"ga\""); - json.Should().Contain("\"lifecycle\": \"deprecated\""); - json.Should().Contain("\"lifecycle\": \"removed\""); + json.Should().MatchInlineJsonSnapshot( + """ + [ + { + "type": "stack", + "sub_type": "stack", + "lifecycle": "preview", + "version": "1.0+" + }, + { + "type": "stack", + "sub_type": "stack", + "lifecycle": "experimental", + "version": "1.0+" + }, + { + "type": "stack", + "sub_type": "stack", + "lifecycle": "beta", + "version": "1.0+" + }, + { + "type": "stack", + "sub_type": "stack", + "lifecycle": "ga", + "version": "1.0+" + }, + { + "type": "stack", + "sub_type": "stack", + "lifecycle": "deprecated", + "version": "1.0+" + }, + { + "type": "stack", + "sub_type": "stack", + "lifecycle": "removed", + "version": "1.0+" + } + ] + """ + ); } [Test] @@ -298,19 +335,30 @@ public void SerializeComplexProducesCorrectJson() var json = JsonSerializer.Serialize(applicableTo, _options); - // Verify it's an array with 3 items - var jsonDoc = JsonDocument.Parse(json); - jsonDoc.RootElement.GetArrayLength().Should().Be(3); - - // Verify each type is present - json.Should().Contain("\"type\": \"stack\""); - json.Should().Contain("\"type\": \"deployment\""); - json.Should().Contain("\"type\": \"product\""); - - // Verify sub_types - json.Should().Contain("\"sub_type\": \"stack\""); - json.Should().Contain("\"sub_type\": \"ece\""); - json.Should().Contain("\"sub_type\": \"product\""); + json.Should().MatchInlineJsonSnapshot( + """ + [ + { + "type": "stack", + "sub_type": "stack", + "lifecycle": "ga", + "version": "8.0+" + }, + { + "type": "deployment", + "sub_type": "ece", + "lifecycle": "ga", + "version": "all" + }, + { + "type": "product", + "sub_type": "product", + "lifecycle": "ga", + "version": "all" + } + ] + """ + ); } [Test] @@ -320,7 +368,7 @@ public void SerializeEmptyProducesEmptyArray() var json = JsonSerializer.Serialize(applicableTo, _options); - json.Should().Be("[]"); + json.Should().MatchInlineJsonSnapshot("[]"); } [Test] @@ -336,24 +384,23 @@ public void SerializeValidatesJsonStructure() }; var json = JsonSerializer.Serialize(original, _options); - var jsonDoc = JsonDocument.Parse(json); - var root = jsonDoc.RootElement; - - root.ValueKind.Should().Be(JsonValueKind.Array); - var array = root.EnumerateArray().ToList(); - - array.Should().HaveCount(2); // Stack + Deployment.Ece - - var stackEntry = array[0]; - stackEntry.GetProperty("type").GetString().Should().Be("stack"); - stackEntry.GetProperty("sub_type").GetString().Should().Be("stack"); - stackEntry.GetProperty("lifecycle").GetString().Should().Be("ga"); - stackEntry.GetProperty("version").GetString().Should().Be("all"); - - var deploymentEntry = array[1]; - deploymentEntry.GetProperty("type").GetString().Should().Be("deployment"); - deploymentEntry.GetProperty("sub_type").GetString().Should().Be("ece"); - deploymentEntry.GetProperty("lifecycle").GetString().Should().Be("beta"); - deploymentEntry.GetProperty("version").GetString().Should().Be("3.0+"); + json.Should().MatchInlineJsonSnapshot( + """ + [ + { + "type": "stack", + "sub_type": "stack", + "lifecycle": "ga", + "version": "all" + }, + { + "type": "deployment", + "sub_type": "ece", + "lifecycle": "beta", + "version": "3.0+" + } + ] + """ + ); } } diff --git a/tests/Elastic.Markdown.Tests/Assembler/AssemblerHtmxMarkdownLinkTests.cs b/tests/Elastic.Markdown.Tests/Assembler/AssemblerHtmxMarkdownLinkTests.cs index e75b388cdc..55d3ecad17 100644 --- a/tests/Elastic.Markdown.Tests/Assembler/AssemblerHtmxMarkdownLinkTests.cs +++ b/tests/Elastic.Markdown.Tests/Assembler/AssemblerHtmxMarkdownLinkTests.cs @@ -7,8 +7,11 @@ using Elastic.Documentation; using Elastic.Documentation.Configuration; using Elastic.Documentation.Diagnostics; +using Elastic.Documentation.Testing; using Elastic.Markdown.IO; using Elastic.Markdown.Tests.Inline; +using Nullean.Snapshots; +using Nullean.Snapshots.Html; namespace Elastic.Markdown.Tests.Assembler; @@ -31,14 +34,14 @@ IConfigurationContext configurationContext }; [Test] - public void CrossLink_HasNoSelectOobButKeepsPreload() - { - Html.Should().NotContain("hx-select-oob"); - Html.Should().Contain("preload=\"mousedown\""); - } - - [Test] - public void CrossLink_NoTargetBlank() => Html.Should().NotContain("target=\"_blank\""); + public void GeneratesHtml() => + Html.Should().MatchInlineHtmlSnapshot( + """ +

Go to + test +

+ """ + ); [Test] public void EmitsCrossLink() @@ -48,7 +51,7 @@ public void EmitsCrossLink() } [Test] - public void HasNoErrors() => Collector.Diagnostics.Should().HaveCount(0); + public void HasNoErrors() => Collector.Diagnostics.ToSnapshotText().Should().MatchInlineSnapshot(""); } [InheritsTests] @@ -67,13 +70,20 @@ IConfigurationContext configurationContext }; [Test] - public void InternalLink_HasNoPerLinkHtmxAttributes() => Html.Should().NotContain("hx-select-oob"); + public void InternalLink_HasNoPerLinkHtmxAttributes() => + Html.Should().MatchInlineHtmlSnapshot( + """ +

+ Requirements +

+ """ + ); [Test] public void EmitsNoCrossLink() => Collector.CrossLinks.Should().HaveCount(0); [Test] - public void HasNoErrors() => Collector.Diagnostics.Should().HaveCount(0); + public void HasNoErrors() => Collector.Diagnostics.ToSnapshotText().Should().MatchInlineSnapshot(""); } [InheritsTests] @@ -94,14 +104,17 @@ IConfigurationContext configurationContext }; [Test] - public void AbsolutePathLink_HasNoSelectOobButKeepsPreload() - { - Html.Should().NotContain("hx-select-oob"); - Html.Should().Contain("preload=\"mousedown\""); - } + public void AbsolutePathLink_HasNoSelectOobButKeepsPreload() => + Html.Should().MatchInlineHtmlSnapshot( + """ +

+ Elasticsearch +

+ """ + ); [Test] - public void HasNoErrors() => Collector.Diagnostics.Should().HaveCount(0); + public void HasNoErrors() => Collector.Diagnostics.ToSnapshotText().Should().MatchInlineSnapshot(""); } [InheritsTests] @@ -124,13 +137,20 @@ IConfigurationContext configurationContext }; [Test] - public void ReferenceLink_HasNoPerLinkHtmxAttributes() => Html.Should().NotContain("hx-select-oob"); + public void ReferenceLink_HasNoPerLinkHtmxAttributes() => + Html.Should().MatchInlineHtmlSnapshot( + """ +

+ test +

+ """ + ); [Test] public void EmitsNoCrossLink() => Collector.CrossLinks.Should().HaveCount(0); [Test] - public void HasNoErrors() => Collector.Diagnostics.Should().HaveCount(0); + public void HasNoErrors() => Collector.Diagnostics.ToSnapshotText().Should().MatchInlineSnapshot(""); } [InheritsTests] @@ -152,14 +172,24 @@ IConfigurationContext configurationContext }; [Test] - public void EmptyTextCrossLink_HasNoPerLinkHtmxAttributes() => Html.Should().NotContain("hx-select-oob"); - - [Test] - public void EmptyTextCrossLink_NoTargetBlank() => Html.Should().NotContain("target=\"_blank\""); + public void GeneratesHtml() => + Html.Should().MatchInlineHtmlSnapshot( + """ +

Go to + +

+ """ + ); [Test] public void HasError() => - Collector.Diagnostics.Should().Contain(d => d.Severity == Severity.Error && d.Message.Contains("empty link text")); + Collector + .Diagnostics + .ToSnapshotText() + .Should() + .MatchInlineSnapshot( + "error {repo}/docs/index.md:4 Crosslink 'kibana://index.md' has empty link text. Please provide explicit link text." + ); [Test] public void EmitsCrossLink() @@ -187,13 +217,20 @@ IConfigurationContext configurationContext }; [Test] - public void InsertPageTitle_HasNoPerLinkHtmxAttributes() => Html.Should().NotContain("hx-select-oob"); + public void InsertPageTitle_HasNoPerLinkHtmxAttributes() => + Html.Should().MatchInlineHtmlSnapshot( + """ +

+ Special Requirements +

+ """ + ); [Test] public void EmitsNoCrossLink() => Collector.CrossLinks.Should().HaveCount(0); [Test] - public void HasNoErrors() => Collector.Diagnostics.Should().HaveCount(0); + public void HasNoErrors() => Collector.Diagnostics.ToSnapshotText().Should().MatchInlineSnapshot(""); } [InheritsTests] @@ -214,13 +251,16 @@ IConfigurationContext configurationContext }; [Test] - public void ExternalLink_DoesNotGetHtmxAttributes_ForAssembler() - { - // HTTP links get target="_blank", not hx-select-oob - Html.Should().Contain("target=\"_blank\""); - Html.Should().NotContain("hx-select-oob"); - } + // HTTP links get target="_blank", not hx-select-oob + public void ExternalLink_DoesNotGetHtmxAttributes_ForAssembler() => + Html.Should().MatchInlineHtmlSnapshot( + """ +

+ link to app +

+ """ + ); [Test] - public void HasNoErrors() => Collector.Diagnostics.Should().HaveCount(0); + public void HasNoErrors() => Collector.Diagnostics.ToSnapshotText().Should().MatchInlineSnapshot(""); } diff --git a/tests/Elastic.Markdown.Tests/CodeBlocks/CallOutTests.cs b/tests/Elastic.Markdown.Tests/CodeBlocks/CallOutTests.cs index b5a28959c9..1b03be55af 100644 --- a/tests/Elastic.Markdown.Tests/CodeBlocks/CallOutTests.cs +++ b/tests/Elastic.Markdown.Tests/CodeBlocks/CallOutTests.cs @@ -4,9 +4,12 @@ using System.IO.Abstractions.TestingHelpers; using AwesomeAssertions; +using Elastic.Documentation.Testing; using Elastic.Markdown.Myst.CodeBlocks; using Elastic.Markdown.Tests.Inline; using JetBrains.Annotations; +using Nullean.Snapshots; +using Nullean.Snapshots.Html; namespace Elastic.Markdown.Tests.CodeBlocks; @@ -47,7 +50,7 @@ public void ParsesMagicCallOuts() => Block!.CallOuts.Should().NotBeNullOrEmpty().And.HaveCount(2).And.NotContain(c => c.Text.Contains("not a callout")); [Test] - public void HasNoErrors() => Collector.Diagnostics.Should().HaveCount(0); + public void HasNoErrors() => Collector.Diagnostics.ToSnapshotText().Should().MatchInlineSnapshot(""); } [InheritsTests] @@ -63,16 +66,30 @@ protected override void AddToFileSystem(MockFileSystem fileSystem) => [Test] public void RendersFormattedInlineMarkdown() => - Html.ShouldContainHtml( + Html.Should().MatchInlineHtmlSnapshot( """ +
+
+
+						var x = 1;
+							
+						
+					
+
+
    -
  1. this uses formatting and a link
  2. +
  3. this uses + formatting + and a + link +
- """ + """, + DocsHtml.Body() ); [Test] - public void HasNoErrors() => Collector.Diagnostics.Should().HaveCount(0); + public void HasNoErrors() => Collector.Diagnostics.ToSnapshotText().Should().MatchInlineSnapshot(""); } [InheritsTests] @@ -90,10 +107,11 @@ public void ParsesMagicCallOuts() => public void RequiresContentToFollow() => Collector .Diagnostics + .ToSnapshotText() .Should() - .HaveCount(1) - .And - .OnlyContain(c => c.Message.StartsWith("Code block with annotations is not followed by any content")); + .MatchInlineSnapshot( + "error {repo}/docs/index.md:3 Code block with annotations is not followed by any content, needs numbered list" + ); } [InheritsTests] @@ -117,10 +135,9 @@ public void ParsesMagicCallOuts() => public void RequiresContentToFollow() => Collector .Diagnostics + .ToSnapshotText() .Should() - .HaveCount(1) - .And - .OnlyContain(c => c.Message.StartsWith("Code block with annotations is not followed by a list")); + .MatchInlineSnapshot("error {repo}/docs/index.md:3 Code block with annotations is not followed by a list"); } [InheritsTests] @@ -146,26 +163,32 @@ public void ParsesMagicCallOuts() => [Test] public void RendersExpectedHtml() => - Html.ShouldBeHtml( + Html.Should().MatchInlineHtmlSnapshot( """
-
var x = 1;
-					  var y = x - 2;
-					  var z = y - 2;
-					
-
+
+						var x = 1;
+							
+							var y = x - 2; var z = y - 2;
+							
+						
+					
+
-

OUTPUT:

+

+ OUTPUT: +

    -
  1. Marking the first callout
  2. -
  3. Marking the second callout
  4. +
  5. Marking the first callout
  6. +
  7. Marking the second callout
- """ + """, + DocsHtml.Body() ); [Test] - public void AllowsAParagraphInBetween() => Collector.Diagnostics.Should().BeEmpty(); + public void AllowsAParagraphInBetween() => Collector.Diagnostics.ToSnapshotText().Should().MatchInlineSnapshot(""); } [InheritsTests] @@ -195,10 +218,11 @@ public void ParsesMagicCallOuts() => public void RequiresContentToFollow() => Collector .Diagnostics + .ToSnapshotText() .Should() - .HaveCount(1) - .And - .OnlyContain(c => c.Message.StartsWith("More than one content block between code block with annotations and its list")); + .MatchInlineSnapshot( + "error {repo}/docs/index.md:3 More than one content block between code block with annotations and its list" + ); } [InheritsTests] @@ -222,10 +246,9 @@ public void ParsesMagicCallOuts() => public void RequiresContentToFollow() => Collector .Diagnostics + .ToSnapshotText() .Should() - .HaveCount(1) - .And - .OnlyContain(c => c.Message.StartsWith("Code block has 2 callouts but the following list only has 1")); + .MatchInlineSnapshot("error {repo}/docs/index.md:3 Code block has 2 callouts but the following list only has 1"); } [InheritsTests] @@ -251,7 +274,7 @@ public void ParsesAllForLineInformation() => Block!.CallOuts.Should().NotBeNullOrEmpty().And.HaveCount(3).And.OnlyContain(c => c.Text.StartsWith('<')); [Test] - public void RequiresContentToFollow() => Collector.Diagnostics.Should().BeEmpty(); + public void RequiresContentToFollow() => Collector.Diagnostics.ToSnapshotText().Should().MatchInlineSnapshot(""); } [InheritsTests] @@ -319,7 +342,7 @@ public void ParsesClassicCallouts() } [Test] - public void HasNoErrors() => Collector.Diagnostics.Should().HaveCount(0); + public void HasNoErrors() => Collector.Diagnostics.ToSnapshotText().Should().MatchInlineSnapshot(""); } [InheritsTests] @@ -341,7 +364,7 @@ public void ParsesMagicCallOuts() => Block!.CallOuts.Should().NotBeNullOrEmpty().And.HaveCount(3).And.OnlyContain(c => c.Text.StartsWith('<')); [Test] - public void HasNoErrors() => Collector.Diagnostics.Should().HaveCount(0); + public void HasNoErrors() => Collector.Diagnostics.ToSnapshotText().Should().MatchInlineSnapshot(""); } [InheritsTests] @@ -366,7 +389,7 @@ public void ParsesMagicCallOuts() => Block!.CallOuts.Should().NotBeNullOrEmpty().And.HaveCount(5).And.OnlyContain(c => c.Text.StartsWith('<')); [Test] - public void HasNoErrors() => Collector.Diagnostics.Should().HaveCount(0); + public void HasNoErrors() => Collector.Diagnostics.ToSnapshotText().Should().MatchInlineSnapshot(""); } [InheritsTests] @@ -391,17 +414,29 @@ public void ParsesCallouts() => Block!.CallOuts.Should().NotBeNullOrEmpty().And.HaveCount(2).And.OnlyContain(c => c.Text.StartsWith('<')); [Test] - public void HandlesCommentBlocksCorrectly() => Collector.Diagnostics.Should().BeEmpty(); + public void HandlesCommentBlocksCorrectly() => Collector.Diagnostics.ToSnapshotText().Should().MatchInlineSnapshot(""); [Test] public void RenderedHtmlContainsCallouts() => - Html.ShouldContainHtml( + Html.Should().MatchInlineHtmlSnapshot( """ +
+
+
+						var x = 1;
+							
+							var y = x - 2; var z = y - 2;
+							
+						
+					
+
+
    -
  1. First callout
  2. -
  3. Second callout
  4. +
  5. First callout
  6. +
  7. Second callout
- """ + """, + DocsHtml.Body() ); } @@ -426,7 +461,7 @@ 2. Second callout public void ParsesCallouts() => Block!.CallOuts.Should().NotBeNullOrEmpty().And.HaveCount(2); [Test] - public void HandlesCommentBlocksCorrectly() => Collector.Diagnostics.Should().BeEmpty(); + public void HandlesCommentBlocksCorrectly() => Collector.Diagnostics.ToSnapshotText().Should().MatchInlineSnapshot(""); } [InheritsTests] @@ -452,18 +487,32 @@ 2. Second callout public void ParsesCallouts() => Block!.CallOuts.Should().NotBeNullOrEmpty().And.HaveCount(2); [Test] - public void HandlesCommentBlocksAndParagraphCorrectly() => Collector.Diagnostics.Should().BeEmpty(); + public void HandlesCommentBlocksAndParagraphCorrectly() => Collector.Diagnostics.ToSnapshotText().Should().MatchInlineSnapshot(""); [Test] public void RendersIntermediateParagraph() => - Html.ShouldContainHtml( + Html.Should().MatchInlineHtmlSnapshot( """ -

This is an intermediate paragraph

+
+
+
+						var x = 1;
+							
+							var y = x - 2; var z = y - 2;
+							
+						
+					
+
+
+

+ This is an intermediate paragraph +

  1. First callout
  2. -
  3. Second callout
  4. +
  5. Second callout
- """ + """, + DocsHtml.Body() ); } @@ -494,10 +543,11 @@ 2. Second callout public void EmitsErrorForTooManyParagraphs() => Collector .Diagnostics + .ToSnapshotText() .Should() - .HaveCount(1) - .And - .OnlyContain(c => c.Message.StartsWith("More than one content block between code block with annotations and its list")); + .MatchInlineSnapshot( + "error {repo}/docs/index.md:3 More than one content block between code block with annotations and its list" + ); } [InheritsTests] @@ -522,10 +572,9 @@ public class CodeBlockWithManyCommentBlocksNoList() : CodeBlockCallOutTests( public void EmitsErrorForNoList() => Collector .Diagnostics + .ToSnapshotText() .Should() - .HaveCount(1) - .And - .OnlyContain(c => c.Message.StartsWith("Code block with annotations is not followed by a list")); + .MatchInlineSnapshot("error {repo}/docs/index.md:3 Code block with annotations is not followed by a list"); } [InheritsTests] @@ -548,8 +597,28 @@ 2. Second callout public void ParsesCallouts() => Block!.CallOuts.Should().NotBeNullOrEmpty().And.HaveCount(2); [Test] - public void HandlesCommentsCorrectly() => Collector.Diagnostics.Should().BeEmpty(); + public void HandlesCommentsCorrectly() => Collector.Diagnostics.ToSnapshotText().Should().MatchInlineSnapshot(""); [Test] - public void RenderedHtmlDoesNotContainComments() => Html.Should().NotContain("basque_keywords"); + public void RenderedHtmlDoesNotContainComments() => + Html.Should().MatchInlineHtmlSnapshot( + """ +
+
+
+					var x = 1;
+						
+						var y = x - 2; var z = y - 2;
+						
+					
+				
+
+
+
    +
  1. First callout
  2. +
  3. Second callout
  4. +
+ """, + DocsHtml.Body() + ); } diff --git a/tests/Elastic.Markdown.Tests/CodeBlocks/CodeBlockArgumentsTests.cs b/tests/Elastic.Markdown.Tests/CodeBlocks/CodeBlockArgumentsTests.cs index 0757e24b50..3d022e0902 100644 --- a/tests/Elastic.Markdown.Tests/CodeBlocks/CodeBlockArgumentsTests.cs +++ b/tests/Elastic.Markdown.Tests/CodeBlocks/CodeBlockArgumentsTests.cs @@ -3,9 +3,12 @@ // See the LICENSE file in the project root for more information using AwesomeAssertions; +using Elastic.Documentation.Testing; using Elastic.Markdown.Myst.CodeBlocks; using Elastic.Markdown.Tests.Inline; using JetBrains.Annotations; +using Nullean.Snapshots; +using Nullean.Snapshots.Html; namespace Elastic.Markdown.Tests.CodeBlocks; @@ -79,10 +82,23 @@ public class DisabledCallouts() : CodeBlockArgumentsTests( ) { [Test] - public void Render() => Html.Should().Contain("<1>"); + public void Render() => + Html.Should().MatchInlineHtmlSnapshot( + """ +
+
+
+					var x = 1; <1> var y = x - 2; var z = y - 2; <2>
+					
+				
+
+
+ """, + DocsHtml.Body() + ); [Test] - public void HasNoErrors() => Collector.Diagnostics.Should().HaveCount(0); + public void HasNoErrors() => Collector.Diagnostics.ToSnapshotText().Should().MatchInlineSnapshot(""); } [InheritsTests] @@ -98,10 +114,27 @@ 1. This is a callout ) { [Test] - public void Render() => Html.Should().Contain(""); + public void Render() => + Html.Should().MatchInlineHtmlSnapshot( + """ +
+
+
+					var x = 1;
+						
+					
+				
+
+
+
    +
  1. This is a callout
  2. +
+ """, + DocsHtml.Body() + ); [Test] - public void HasNoErrors() => Collector.Diagnostics.Should().HaveCount(0); + public void HasNoErrors() => Collector.Diagnostics.ToSnapshotText().Should().MatchInlineSnapshot(""); } [InheritsTests] @@ -117,10 +150,26 @@ 1. This is a callout ) { [Test] - public void Render() => Html.Should().Contain("This is a variable"); + public void Render() => + Html.Should().MatchInlineHtmlSnapshot( + """ +
+
+
+					This is a variable
+					
+				
+
+
+
    +
  1. This is a callout
  2. +
+ """, + DocsHtml.Body() + ); [Test] - public void HasNoErrors() => Collector.Diagnostics.Should().HaveCount(0); + public void HasNoErrors() => Collector.Diagnostics.ToSnapshotText().Should().MatchInlineSnapshot(""); } [InheritsTests] @@ -136,10 +185,26 @@ 1. This is a callout ) { [Test] - public void Render() => Html.Should().Contain("{{a-variable}}"); + public void Render() => + Html.Should().MatchInlineHtmlSnapshot( + """ +
+
+
+					{{a-variable}}
+					
+				
+
+
+
    +
  1. This is a callout
  2. +
+ """, + DocsHtml.Body() + ); [Test] - public void HasNoErrors() => Collector.Diagnostics.Should().HaveCount(0); + public void HasNoErrors() => Collector.Diagnostics.ToSnapshotText().Should().MatchInlineSnapshot(""); } [InheritsTests] @@ -155,8 +220,24 @@ 1. This is a callout ) { [Test] - public void Render() => Html.Should().Contain("This is a variable").And.Contain("<1>"); + public void Render() => + Html.Should().MatchInlineHtmlSnapshot( + """ +
+
+
+					This is a variable <1>
+					
+				
+
+
+
    +
  1. This is a callout
  2. +
+ """, + DocsHtml.Body() + ); [Test] - public void HasNoErrors() => Collector.Diagnostics.Should().HaveCount(0); + public void HasNoErrors() => Collector.Diagnostics.ToSnapshotText().Should().MatchInlineSnapshot(""); } diff --git a/tests/Elastic.Markdown.Tests/CodeBlocks/ConsoleCodeBlockTests.cs b/tests/Elastic.Markdown.Tests/CodeBlocks/ConsoleCodeBlockTests.cs index eeb4d93d95..0958b3c372 100644 --- a/tests/Elastic.Markdown.Tests/CodeBlocks/ConsoleCodeBlockTests.cs +++ b/tests/Elastic.Markdown.Tests/CodeBlocks/ConsoleCodeBlockTests.cs @@ -3,9 +3,11 @@ // See the LICENSE file in the project root for more information using AwesomeAssertions; +using Elastic.Documentation.Testing; using Elastic.Markdown.Myst.CodeBlocks; using Elastic.Markdown.Tests.Inline; using JetBrains.Annotations; +using Nullean.Snapshots; namespace Elastic.Markdown.Tests.CodeBlocks; @@ -50,7 +52,7 @@ public void CreatesSingleApiSegment() } [Test] - public void HasNoErrors() => Collector.Diagnostics.Should().BeEmpty(); + public void HasNoErrors() => Collector.Diagnostics.ToSnapshotText().Should().MatchInlineSnapshot(""); } [InheritsTests] @@ -101,7 +103,7 @@ public void CreatesMultipleApiSegments() } [Test] - public void HasNoErrors() => Collector.Diagnostics.Should().BeEmpty(); + public void HasNoErrors() => Collector.Diagnostics.ToSnapshotText().Should().MatchInlineSnapshot(""); } [InheritsTests] @@ -135,7 +137,7 @@ public void HandlesDifferentHttpVerbs() } [Test] - public void HasNoErrors() => Collector.Diagnostics.Should().BeEmpty(); + public void HasNoErrors() => Collector.Diagnostics.ToSnapshotText().Should().MatchInlineSnapshot(""); } [InheritsTests] @@ -169,7 +171,7 @@ public void CreatesMultipleApiSegmentsWithCallouts() } [Test] - public void HasNoErrors() => Collector.Diagnostics.Should().BeEmpty(); + public void HasNoErrors() => Collector.Diagnostics.ToSnapshotText().Should().MatchInlineSnapshot(""); } [InheritsTests] @@ -198,7 +200,7 @@ public void HandlesEmptyLinesBetweenApiCalls() } [Test] - public void HasNoErrors() => Collector.Diagnostics.Should().BeEmpty(); + public void HasNoErrors() => Collector.Diagnostics.ToSnapshotText().Should().MatchInlineSnapshot(""); } [InheritsTests] @@ -224,7 +226,7 @@ public void HandlesApiCallsWithoutBodies() } [Test] - public void HasNoErrors() => Collector.Diagnostics.Should().BeEmpty(); + public void HasNoErrors() => Collector.Diagnostics.ToSnapshotText().Should().MatchInlineSnapshot(""); } [InheritsTests] @@ -282,7 +284,7 @@ public void RendersCalloutHtmlInConsoleCodeBlocks() } [Test] - public void HasNoErrors() => Collector.Diagnostics.Should().BeEmpty(); + public void HasNoErrors() => Collector.Diagnostics.ToSnapshotText().Should().MatchInlineSnapshot(""); } [InheritsTests] @@ -348,7 +350,7 @@ public void RendersCalloutsInJsonContent() } [Test] - public void HasNoErrors() => Collector.Diagnostics.Should().BeEmpty(); + public void HasNoErrors() => Collector.Diagnostics.ToSnapshotText().Should().MatchInlineSnapshot(""); } [InheritsTests] @@ -424,5 +426,5 @@ public void EscapesHtmlCharsInHeader() } [Test] - public void HasNoErrors() => Collector.Diagnostics.Should().BeEmpty(); + public void HasNoErrors() => Collector.Diagnostics.ToSnapshotText().Should().MatchInlineSnapshot(""); } diff --git a/tests/Elastic.Markdown.Tests/Codex/CodexHtmxCrossLinkTests.cs b/tests/Elastic.Markdown.Tests/Codex/CodexHtmxCrossLinkTests.cs index 75a6cbd3ae..0435d308e5 100644 --- a/tests/Elastic.Markdown.Tests/Codex/CodexHtmxCrossLinkTests.cs +++ b/tests/Elastic.Markdown.Tests/Codex/CodexHtmxCrossLinkTests.cs @@ -7,8 +7,11 @@ using Elastic.Documentation; using Elastic.Documentation.Configuration; using Elastic.Documentation.Links.CrossLinks; +using Elastic.Documentation.Testing; using Elastic.Markdown.IO; using Elastic.Markdown.Tests.Inline; +using Nullean.Snapshots; +using Nullean.Snapshots.Html; namespace Elastic.Markdown.Tests.Codex; @@ -30,24 +33,17 @@ IConfigurationContext configurationContext protected override ICrossLinkResolver CreateCrossLinkResolver() => new TestCodexCrossLinkResolver(useRelativePaths: true); [Test] - public void CrossLink_ProducesPathOnlyHref() - { - Html.Should().Contain("href=\"/r/kibana/\""); - Html.Should().NotContain("https://codex.elastic.dev"); - } + public void GeneratesHtml() => + Html.Should().MatchInlineHtmlSnapshot( + """ +

Go to + test +

+ """ + ); [Test] - public void CrossLink_HasNoSelectOobButKeepsPreload() - { - Html.Should().NotContain("hx-select-oob"); - Html.Should().Contain("preload=\"mousedown\""); - } - - [Test] - public void CrossLink_NoTargetBlank() => Html.Should().NotContain("target=\"_blank\""); - - [Test] - public void HasNoErrors() => Collector.Diagnostics.Should().HaveCount(0); + public void HasNoErrors() => Collector.Diagnostics.ToSnapshotText().Should().MatchInlineSnapshot(""); } [InheritsTests] @@ -68,14 +64,15 @@ IConfigurationContext configurationContext protected override ICrossLinkResolver CreateCrossLinkResolver() => new TestCodexCrossLinkResolver(useRelativePaths: false); [Test] - public void IsolatedCrossLink_HasAbsoluteHref() => Html.Should().Contain("https://codex.elastic.dev/r/kibana/"); - - [Test] - public void IsolatedCrossLink_HasTargetBlank() => Html.Should().Contain("target=\"_blank\""); - - [Test] - public void IsolatedCrossLink_NoHtmx() => Html.Should().NotContain("hx-select-oob"); + public void GeneratesHtml() => + Html.Should().MatchInlineHtmlSnapshot( + """ +

Go to + test +

+ """ + ); [Test] - public void HasNoErrors() => Collector.Diagnostics.Should().HaveCount(0); + public void HasNoErrors() => Collector.Diagnostics.ToSnapshotText().Should().MatchInlineSnapshot(""); } diff --git a/tests/Elastic.Markdown.Tests/DocSet/RepositoryLinksTests.cs b/tests/Elastic.Markdown.Tests/DocSet/RepositoryLinksTests.cs index 565693f736..aaf653ff11 100644 --- a/tests/Elastic.Markdown.Tests/DocSet/RepositoryLinksTests.cs +++ b/tests/Elastic.Markdown.Tests/DocSet/RepositoryLinksTests.cs @@ -6,6 +6,7 @@ using Elastic.Documentation; using Elastic.Documentation.Configuration; using Elastic.Documentation.Links; +using Nullean.Snapshots.Json; namespace Elastic.Markdown.Tests.DocSet; @@ -59,7 +60,7 @@ public void SerializesCurrent() }; var json = RepositoryLinks.Serialize(linkReference); // language=json - json.Should().Be( + json.Should().MatchInlineJsonSnapshot( """ { "origin": { diff --git a/tests/Elastic.Markdown.Tests/Exporters/OkfMarkdownExporterTests.cs b/tests/Elastic.Markdown.Tests/Exporters/OkfMarkdownExporterTests.cs index 3168193a80..f45996ba24 100644 --- a/tests/Elastic.Markdown.Tests/Exporters/OkfMarkdownExporterTests.cs +++ b/tests/Elastic.Markdown.Tests/Exporters/OkfMarkdownExporterTests.cs @@ -6,6 +6,7 @@ using Elastic.Documentation.Navigation; using Elastic.Markdown; using Elastic.Markdown.Exporters; +using Nullean.Snapshots; using SharpYaml; namespace Elastic.Markdown.Tests.Exporters; @@ -233,7 +234,11 @@ public void RenderIndexContent_RootDirectory_DeclaresOkfVersionAndNoOtherFrontma { var content = OkfMarkdownExporter.RenderIndexContent(directory: "", concepts: [], subdirectories: []); - content.Should().StartWith("---\nokf_version: \"0.1\"\n---"); + content.Should().MatchInlineSnapshot(""" + --- + okf_version: "0.1" + --- + """); } [Test] @@ -241,8 +246,7 @@ public void RenderIndexContent_NonRootDirectory_HasNoFrontmatter() { var content = OkfMarkdownExporter.RenderIndexContent(directory: "reference", concepts: [], subdirectories: []); - content.Should().NotContain("---"); - content.Should().NotContain("okf_version"); + content.Should().MatchInlineSnapshot(""); } [Test] @@ -257,11 +261,18 @@ public void RenderIndexContent_WithConceptsAndSubdirectories_GroupsThemUnderSepa var content = OkfMarkdownExporter.RenderIndexContent(directory: "reference", concepts: concepts, subdirectories: ["reference/foo"]); - content.Should().Contain("# Documents"); - content.Should().Contain("* [Bar](bar.md) - Bar description"); - content.Should().Contain("* [Foo](foo.md) - Foo description"); - content.Should().Contain("# Subdirectories"); - content.Should().Contain("* [foo](foo/) - Foo description"); + content.Should().MatchInlineSnapshot( + """ + # Documents + + * [Bar](bar.md) - Bar description + * [Foo](foo.md) - Foo description + + # Subdirectories + + * [foo](foo/) - Foo description + """ + ); } [Test] @@ -269,8 +280,11 @@ public void RenderIndexContent_SubdirectoryWithoutSiblingLandingPage_OmitsDescri { var content = OkfMarkdownExporter.RenderIndexContent(directory: "reference", concepts: [], subdirectories: ["reference/foo"]); - content.Should().Contain("* [foo](foo/)"); - content.Should().NotContain("* [foo](foo/) -"); + content.Should().MatchInlineSnapshot(""" + # Subdirectories + + * [foo](foo/) + """); } [Test] diff --git a/tests/Elastic.Markdown.Tests/FileInclusion/HeadingOrderTests.cs b/tests/Elastic.Markdown.Tests/FileInclusion/HeadingOrderTests.cs index 9b451c4cc2..8495ba5d98 100644 --- a/tests/Elastic.Markdown.Tests/FileInclusion/HeadingOrderTests.cs +++ b/tests/Elastic.Markdown.Tests/FileInclusion/HeadingOrderTests.cs @@ -4,10 +4,13 @@ using System.IO.Abstractions.TestingHelpers; using AwesomeAssertions; +using Elastic.Documentation.Testing; using Elastic.Markdown.IO; using Elastic.Markdown.Myst.Directives.Include; using Elastic.Markdown.Myst.Directives.Stepper; using Elastic.Markdown.Tests.Directives; +using Nullean.Snapshots; +using Nullean.Snapshots.Html; namespace Elastic.Markdown.Tests.FileInclusion; @@ -40,7 +43,7 @@ protected override void AddToFileSystem(MockFileSystem fileSystem) public void ParsesBlock() => Block.Should().NotBeNull(); [Test] - public void IncludesSnippetAfterMainContent() => Html.Should().Contain("Two").And.Contain("Six"); + public void IncludesSnippetAfterMainContent() => Html.Should().MatchHtmlSnapshot(DocsHtml.Body()); [Test] public void TableOfContentsRespectsOrder() @@ -861,11 +864,10 @@ public void InternalHeadingIsAdjustedToOneLevelDeeper() public void HintIsEmittedForAdjustedHeading() => Collector .Diagnostics + .ToSnapshotText() .Should() - .ContainSingle( - d => d.Severity == Documentation.Diagnostics.Severity.Hint && d.Message.Contains("h3") && d.Message.Contains( - "h4" - ) && d.Message.Contains("####") + .MatchInlineSnapshot( + "hint {repo}/docs/index.md:8 Heading level h3 inside a step renders at the same or higher level as the step itself (h3). It has been adjusted to h4 — write it as '####' to avoid this hint." ); } @@ -908,7 +910,7 @@ public void DeepHeadingIsUntouched() } [Test] - public void NoHintEmittedForValidHeading() => Collector.Diagnostics.Should().BeEmpty(); + public void NoHintEmittedForValidHeading() => Collector.Diagnostics.ToSnapshotText().Should().MatchInlineSnapshot(""); } /// diff --git a/tests/Elastic.Markdown.Tests/FileInclusion/IncludeTests.cs b/tests/Elastic.Markdown.Tests/FileInclusion/IncludeTests.cs index 325151d68f..541a996a19 100644 --- a/tests/Elastic.Markdown.Tests/FileInclusion/IncludeTests.cs +++ b/tests/Elastic.Markdown.Tests/FileInclusion/IncludeTests.cs @@ -5,8 +5,11 @@ using System.IO.Abstractions.TestingHelpers; using AwesomeAssertions; using Elastic.Documentation.Diagnostics; +using Elastic.Documentation.Testing; using Elastic.Markdown.Myst.Directives.Include; using Elastic.Markdown.Tests.Directives; +using Nullean.Snapshots; +using Nullean.Snapshots.Html; namespace Elastic.Markdown.Tests.FileInclusion; @@ -27,7 +30,7 @@ protected override void AddToFileSystem(MockFileSystem fileSystem) public void ParsesBlock() => Block.Should().NotBeNull(); [Test] - public void IncludesInclusionHtml() => Html.ShouldBeHtml("

Hello world

"); + public void IncludesInclusionHtml() => Html.Should().MatchInlineHtmlSnapshot("

Hello world

", DocsHtml.Body()); } [InheritsTests] @@ -67,12 +70,12 @@ public class IncludeNotFoundTests() : DirectiveTest(""" public void IncludesNothing() => Html.Should().Be(""); [Test] - public void EmitsError() - { - Collector.Diagnostics.Should().NotBeNullOrEmpty().And.HaveCount(1); - Collector.Diagnostics.Should().OnlyContain(d => d.Severity == Severity.Error); - Collector.Diagnostics.Should().OnlyContain(d => d.Message.Contains("notfound.md` does not exist")); - } + public void EmitsError() => + Collector + .Diagnostics + .ToSnapshotText() + .Should() + .MatchInlineSnapshot("error {repo}/docs/index.md:3 `{repo}/docs/_snippets/notfound.md` does not exist."); } [InheritsTests] @@ -88,12 +91,8 @@ public class IncludeRequiresArgument() : DirectiveTest(""" public void IncludesNothing() => Html.Should().Be(""); [Test] - public void EmitsError() - { - Collector.Diagnostics.Should().NotBeNullOrEmpty().And.HaveCount(1); - Collector.Diagnostics.Should().OnlyContain(d => d.Severity == Severity.Error); - Collector.Diagnostics.Should().OnlyContain(d => d.Message.Contains("include requires an argument.")); - } + public void EmitsError() => + Collector.Diagnostics.ToSnapshotText().Should().MatchInlineSnapshot("error {repo}/docs/index.md:3 include requires an argument."); } [InheritsTests] @@ -116,14 +115,17 @@ protected override void AddToFileSystem(MockFileSystem fileSystem) public void IncludesNothing() => Html.Should().Be(""); [Test] - public void EmitsError() - { - Collector.Diagnostics.Should().NotBeNullOrEmpty(); + public void EmitsError() => Collector .Diagnostics + .ToSnapshotText() .Should() - .Contain(d => d.Severity == Severity.Error && d.Message.Contains("only supports including snippets from `_snippet` folders.")); - } + .MatchInlineSnapshot( + """ + warning {repo}/docs/test.md Document has no title, using file name as title. + error {repo}/docs/index.md:3 {include} only supports including snippets from `_snippet` folders. `{repo}/docs/test.md` is not a snippet + """ + ); } [InheritsTests] @@ -135,14 +137,12 @@ public class IncludeRelativeTraversalBlocked() : DirectiveTest(""" protected override void AddToFileSystem(MockFileSystem fileSystem) => fileSystem.AddFile(@"outside.txt", "some content"); [Test] - public void EmitsError() - { - Collector.Diagnostics.Should().NotBeNullOrEmpty(); + public void EmitsError() => Collector .Diagnostics + .ToSnapshotText() .Should() - .Contain(d => d.Severity == Severity.Error && d.Message.Contains("must resolve within the documentation source directory")); - } + .MatchInlineSnapshot("error {repo}/docs/index.md:3 Include path must resolve within the documentation source directory."); } [InheritsTests] @@ -168,10 +168,12 @@ protected override void AddToFileSystem(MockFileSystem fileSystem) public void IncludesNothing() => Html.Should().Be(""); [Test] - public void EmitsError() - { - Collector.Diagnostics.Should().NotBeNullOrEmpty().And.HaveCount(1); - Collector.Diagnostics.Should().OnlyContain(d => d.Severity == Severity.Error); - Collector.Diagnostics.Should().Contain(d => d.Message.Contains("cyclical include detected")); - } + public void EmitsError() => + Collector + .Diagnostics + .ToSnapshotText() + .Should() + .MatchInlineSnapshot( + "error {repo}/docs/_snippets/test.md:1 {include} cyclical include detected `{repo}/docs/_snippets/test.md` points to itself" + ); } diff --git a/tests/Elastic.Markdown.Tests/FileInclusion/IncludedAppliesSwitchTests.cs b/tests/Elastic.Markdown.Tests/FileInclusion/IncludedAppliesSwitchTests.cs index 7c75bf1ab9..107a038bc1 100644 --- a/tests/Elastic.Markdown.Tests/FileInclusion/IncludedAppliesSwitchTests.cs +++ b/tests/Elastic.Markdown.Tests/FileInclusion/IncludedAppliesSwitchTests.cs @@ -4,8 +4,10 @@ using System.IO.Abstractions.TestingHelpers; using AwesomeAssertions; +using Elastic.Documentation.Testing; using Elastic.Markdown.Myst.Directives.Include; using Elastic.Markdown.Tests.Directives; +using Nullean.Snapshots.Html; namespace Elastic.Markdown.Tests.FileInclusion; @@ -47,18 +49,9 @@ protected override void AddToFileSystem(MockFileSystem fileSystem) public void ParsesBlock() => Block.Should().NotBeNull(); [Test] - public void EachIncludeHasUniqueIds() - { - // First include at line 2: (2 * 1000) + 0 = 2000 - // Second include at line 7: (7 * 1000) + 0 = 7000 - Html.Should().Contain("applies-switch-item-2000-0"); - Html.Should().Contain("applies-switch-item-2000-1"); - Html.Should().Contain("applies-switch-set-2000"); - - Html.Should().Contain("applies-switch-item-7000-0"); - Html.Should().Contain("applies-switch-item-7000-1"); - Html.Should().Contain("applies-switch-set-7000"); - } + // First include at line 2: (2 * 1000) + 0 = 2000 + // Second include at line 7: (7 * 1000) + 0 = 7000 + public void EachIncludeHasUniqueIds() => Html.Should().MatchHtmlSnapshot(DocsHtml.Body()); } /// @@ -96,11 +89,35 @@ Some content between. public void ParsesBlock() => Block.Should().NotBeNull(); [Test] - public void EachAppliesSwitchHasUniqueIds() - { - // Include at line 2, first applies-switch at line 0: (2 * 1000) + 0 = 2000 - // Include at line 2, second applies-switch at line 8: (2 * 1000) + 8 = 2008 - Html.Should().Contain("applies-switch-set-2000"); - Html.Should().Contain("applies-switch-set-2008"); - } + // Include at line 2, first applies-switch at line 0: (2 * 1000) + 0 = 2000 + // Include at line 2, second applies-switch at line 8: (2 * 1000) + 8 = 2008 + public void EachAppliesSwitchHasUniqueIds() => + Html.Should().MatchInlineHtmlSnapshot( + """ +
+ + +
+

First switch - Stack

+
+
+

Some content between.

+
+ + +
+

Second switch - Serverless

+
+
+ """, + DocsHtml.Body() + ); } diff --git a/tests/Elastic.Markdown.Tests/FileInclusion/IncludedTabSetTests.cs b/tests/Elastic.Markdown.Tests/FileInclusion/IncludedTabSetTests.cs index 853be6f911..38860fd128 100644 --- a/tests/Elastic.Markdown.Tests/FileInclusion/IncludedTabSetTests.cs +++ b/tests/Elastic.Markdown.Tests/FileInclusion/IncludedTabSetTests.cs @@ -4,8 +4,10 @@ using System.IO.Abstractions.TestingHelpers; using AwesomeAssertions; +using Elastic.Documentation.Testing; using Elastic.Markdown.Myst.Directives.Include; using Elastic.Markdown.Tests.Directives; +using Nullean.Snapshots.Html; namespace Elastic.Markdown.Tests.FileInclusion; @@ -47,18 +49,9 @@ protected override void AddToFileSystem(MockFileSystem fileSystem) public void ParsesBlock() => Block.Should().NotBeNull(); [Test] - public void EachIncludeHasUniqueIds() - { - // First include at line 2: (2 * 1000) + 0 = 2000 - // Second include at line 7: (7 * 1000) + 0 = 7000 - Html.Should().Contain("tabs-item-2000-0"); - Html.Should().Contain("tabs-item-2000-1"); - Html.Should().Contain("tabs-set-2000"); - - Html.Should().Contain("tabs-item-7000-0"); - Html.Should().Contain("tabs-item-7000-1"); - Html.Should().Contain("tabs-set-7000"); - } + // First include at line 2: (2 * 1000) + 0 = 2000 + // Second include at line 7: (7 * 1000) + 0 = 7000 + public void EachIncludeHasUniqueIds() => Html.Should().MatchHtmlSnapshot(DocsHtml.Body()); } /// @@ -96,11 +89,27 @@ Second tab set public void ParsesBlock() => Block.Should().NotBeNull(); [Test] - public void EachTabSetHasUniqueIds() - { - // Include at line 2, first tab-set at line 0: (2 * 1000) + 0 = 2000 - // Include at line 2, second tab-set at line 8: (2 * 1000) + 8 = 2008 - Html.Should().Contain("tabs-set-2000"); - Html.Should().Contain("tabs-set-2008"); - } + // Include at line 2, first tab-set at line 0: (2 * 1000) + 0 = 2000 + // Include at line 2, second tab-set at line 8: (2 * 1000) + 8 = 2008 + public void EachTabSetHasUniqueIds() => + Html.Should().MatchInlineHtmlSnapshot( + """ +
+ + +
+

First tab set

+
+
+

Some content between.

+
+ + +
+

Second tab set

+
+
+ """, + DocsHtml.Body() + ); } diff --git a/tests/Elastic.Markdown.Tests/FileInclusion/LiteralIncludeTests.cs b/tests/Elastic.Markdown.Tests/FileInclusion/LiteralIncludeTests.cs index 1b26bd9e04..7a6158af59 100644 --- a/tests/Elastic.Markdown.Tests/FileInclusion/LiteralIncludeTests.cs +++ b/tests/Elastic.Markdown.Tests/FileInclusion/LiteralIncludeTests.cs @@ -5,8 +5,10 @@ using System.IO.Abstractions.TestingHelpers; using AwesomeAssertions; using Elastic.Documentation.Diagnostics; +using Elastic.Documentation.Testing; using Elastic.Markdown.Myst.Directives.Include; using Elastic.Markdown.Tests.Directives; +using Nullean.Snapshots; namespace Elastic.Markdown.Tests.FileInclusion; @@ -60,14 +62,12 @@ public class LiteralIncludeRelativeTraversalBlocked() : DirectiveTest fileSystem.AddFile(@"outside.txt", "some content"); [Test] - public void EmitsError() - { - Collector.Diagnostics.Should().NotBeNullOrEmpty(); + public void EmitsError() => Collector .Diagnostics + .ToSnapshotText() .Should() - .Contain(d => d.Severity == Severity.Error && d.Message.Contains("must resolve within the documentation source directory")); - } + .MatchInlineSnapshot("error {repo}/docs/index.md:3 Include path must resolve within the documentation source directory."); } [InheritsTests] @@ -79,14 +79,12 @@ public class LiteralIncludeAbsoluteTraversalBlocked() : DirectiveTest fileSystem.AddFile(@"outside.txt", "some content"); [Test] - public void EmitsError() - { - Collector.Diagnostics.Should().NotBeNullOrEmpty(); + public void EmitsError() => Collector .Diagnostics + .ToSnapshotText() .Should() - .Contain(d => d.Severity == Severity.Error && d.Message.Contains("must resolve within the documentation source directory")); - } + .MatchInlineSnapshot("error {repo}/docs/index.md:3 Include path must resolve within the documentation source directory."); } [InheritsTests] @@ -98,12 +96,10 @@ public class LiteralIncludeHiddenDirectoryBlocked() : DirectiveTest fileSystem.AddFile(@"docs/.config/data.txt", "some content"); [Test] - public void EmitsError() - { - Collector.Diagnostics.Should().NotBeNullOrEmpty(); + public void EmitsError() => Collector .Diagnostics + .ToSnapshotText() .Should() - .Contain(d => d.Severity == Severity.Error && d.Message.Contains("must not traverse hidden directories")); - } + .MatchInlineSnapshot("error {repo}/docs/index.md:3 Path must not traverse hidden directories."); } diff --git a/tests/Elastic.Markdown.Tests/FrontMatter/YamlFrontMatterTests.cs b/tests/Elastic.Markdown.Tests/FrontMatter/YamlFrontMatterTests.cs index e3d3608dd3..559b339dab 100644 --- a/tests/Elastic.Markdown.Tests/FrontMatter/YamlFrontMatterTests.cs +++ b/tests/Elastic.Markdown.Tests/FrontMatter/YamlFrontMatterTests.cs @@ -5,10 +5,12 @@ using System.IO.Abstractions.TestingHelpers; using AwesomeAssertions; using Elastic.Documentation.Diagnostics; +using Elastic.Documentation.Testing; using Elastic.Markdown.IO; using Elastic.Markdown.Myst; using Elastic.Markdown.Myst.FrontMatter; using Elastic.Markdown.Tests.Directives; +using Nullean.Snapshots; namespace Elastic.Markdown.Tests.FrontMatter; @@ -191,18 +193,17 @@ public class AllUnknownFrontMatterKeysAreHints() : DirectiveTest( ) { [Test] - public void ReportsOneHintPerKeyAndNoErrors() - { - Collector.Errors.Should().Be(0); - Collector.Diagnostics.Should().HaveCount(2).And.OnlyContain(d => d.Severity == Severity.Hint); + public void ReportsOneHintPerKeyAndNoErrors() => Collector .Diagnostics - .Select(d => d.Message) + .ToSnapshotText() .Should() - .Contain(m => m.Contains("Unknown key 'type'")) - .And - .Contain(m => m.Contains("Unknown key 'comment'")); - } + .MatchInlineSnapshot( + """ + hint {repo}/docs/index.md:2 Unknown key 'type'. Valid keys: applies_to, cta, description, layout, listing, mapped_pages, meta_title, navigation_title, noindex, products, sub, title. It is ignored. + hint {repo}/docs/index.md:3 Unknown key 'comment'. Valid keys: applies_to, cta, description, layout, listing, mapped_pages, meta_title, navigation_title, noindex, products, sub, title. It is ignored. + """ + ); } public class YamlFrontMatterTests() : DirectiveTest( @@ -245,7 +246,11 @@ public class EmptyFileWarnsNeedingATitle() : DirectiveTest("") [Test] public void WarnsOfNoTitle() => - Collector.Diagnostics.Should().NotBeEmpty().And.Contain(d => d.Message.Contains("Document has no title, using file name as title.")); + Collector + .Diagnostics + .ToSnapshotText() + .Should() + .MatchInlineSnapshot("warning {repo}/docs/index.md Document has no title, using file name as title."); } public class NavigationTitleSupportReplacements() : DirectiveTest( @@ -408,7 +413,7 @@ public class MappedPagesValidUrl() : DirectiveTest( ) { [Test] - public void NoErrors() => Collector.Diagnostics.Should().BeEmpty(); + public void NoErrors() => Collector.Diagnostics.ToSnapshotText().Should().MatchInlineSnapshot(""); } public class MappedPagesInvalidUrl() : DirectiveTest( @@ -423,18 +428,14 @@ public class MappedPagesInvalidUrl() : DirectiveTest( ) { [Test] - public void HasErrors() - { - Collector.Diagnostics.Should().HaveCount(1); + public void HasErrors() => Collector .Diagnostics + .ToSnapshotText() .Should() - .Contain( - d => d.Message.Contains( - "Invalid mapped_pages URL: \"https://www.elastic.co/docs/get-started/deployment-options\". All mapped_pages URLs must start with \"https://www.elastic.co/guide\". Please update the URL to reference content under the Elastic documentation guide." - ) + .MatchInlineSnapshot( + "error {repo}/docs/index.md Invalid mapped_pages URL: \"https://www.elastic.co/docs/get-started/deployment-options\". All mapped_pages URLs must start with \"https://www.elastic.co/guide\". Please update the URL to reference content under the Elastic documentation guide." ); - } } public class MappedPagesMixedUrls() : DirectiveTest( @@ -451,18 +452,14 @@ public class MappedPagesMixedUrls() : DirectiveTest( ) { [Test] - public void HasErrorsForInvalidUrl() - { - Collector.Diagnostics.Should().HaveCount(1); + public void HasErrorsForInvalidUrl() => Collector .Diagnostics + .ToSnapshotText() .Should() - .Contain( - d => d.Message.Contains( - "Invalid mapped_pages URL: \"https://www.elastic.co/docs/invalid-url\". All mapped_pages URLs must start with \"https://www.elastic.co/guide\"" - ) + .MatchInlineSnapshot( + "error {repo}/docs/index.md Invalid mapped_pages URL: \"https://www.elastic.co/docs/invalid-url\". All mapped_pages URLs must start with \"https://www.elastic.co/guide\". Please update the URL to reference content under the Elastic documentation guide." ); - } } public class MappedPagesEmptyUrl() : DirectiveTest(""" @@ -475,11 +472,8 @@ public class MappedPagesEmptyUrl() : DirectiveTest(""" """) { [Test] - public void NoErrorsForEmptyUrl() - { - // Empty URLs are ignored, no validation error should occur - Collector.Diagnostics.Should().BeEmpty(); - } + // Empty URLs are ignored, no validation error should occur + public void NoErrorsForEmptyUrl() => Collector.Diagnostics.ToSnapshotText().Should().MatchInlineSnapshot(""); } public class MappedPagesExternalUrl() : DirectiveTest( @@ -494,18 +488,14 @@ public class MappedPagesExternalUrl() : DirectiveTest( ) { [Test] - public void HasErrorsForExternalUrl() - { - Collector.Diagnostics.Should().HaveCount(1); + public void HasErrorsForExternalUrl() => Collector .Diagnostics + .ToSnapshotText() .Should() - .Contain( - d => d.Message.Contains( - "Invalid mapped_pages URL: \"https://github.com/elastic/docs-builder\". All mapped_pages URLs must start with \"https://www.elastic.co/guide\"" - ) + .MatchInlineSnapshot( + "error {repo}/docs/index.md Invalid mapped_pages URL: \"https://github.com/elastic/docs-builder\". All mapped_pages URLs must start with \"https://www.elastic.co/guide\". Please update the URL to reference content under the Elastic documentation guide." ); - } } public class MappedPagesMalformedUri() : DirectiveTest( @@ -520,18 +510,14 @@ public class MappedPagesMalformedUri() : DirectiveTest( ) { [Test] - public void HasErrorsForMalformedUri() - { - Collector.Diagnostics.Should().HaveCount(1); + public void HasErrorsForMalformedUri() => Collector .Diagnostics + .ToSnapshotText() .Should() - .Contain( - d => d.Message.Contains( - "Invalid mapped_pages URL: \"https://www.elastic.co/guide/[invalid-characters]\". All mapped_pages URLs must start with \"https://www.elastic.co/guide\"" - ) + .MatchInlineSnapshot( + "error {repo}/docs/index.md Invalid mapped_pages URL: \"https://www.elastic.co/guide/[invalid-characters]\". All mapped_pages URLs must start with \"https://www.elastic.co/guide\". Please update the URL to reference content under the Elastic documentation guide." ); - } } public class MappedPagesInvalidScheme() : DirectiveTest( @@ -546,18 +532,14 @@ public class MappedPagesInvalidScheme() : DirectiveTest( ) { [Test] - public void HasErrorsForInvalidScheme() - { - Collector.Diagnostics.Should().HaveCount(1); + public void HasErrorsForInvalidScheme() => Collector .Diagnostics + .ToSnapshotText() .Should() - .Contain( - d => d.Message.Contains( - "Invalid mapped_pages URL: \"https://www.elastic.co/guide/invalid uri with spaces\". All mapped_pages URLs must start with \"https://www.elastic.co/guide\"" - ) + .MatchInlineSnapshot( + "error {repo}/docs/index.md Invalid mapped_pages URL: \"https://www.elastic.co/guide/invalid uri with spaces\". All mapped_pages URLs must start with \"https://www.elastic.co/guide\". Please update the URL to reference content under the Elastic documentation guide." ); - } } public class MappedPagesNotAbsoluteUri() : DirectiveTest(""" @@ -570,16 +552,12 @@ public class MappedPagesNotAbsoluteUri() : DirectiveTest(""" """) { [Test] - public void HasErrorsForNotAbsoluteUri() - { - Collector.Diagnostics.Should().HaveCount(1); + public void HasErrorsForNotAbsoluteUri() => Collector .Diagnostics + .ToSnapshotText() .Should() - .Contain( - d => d.Message.Contains( - "Invalid mapped_pages URL: \"not-a-uri-at-all\". All mapped_pages URLs must start with \"https://www.elastic.co/guide\"" - ) + .MatchInlineSnapshot( + "error {repo}/docs/index.md Invalid mapped_pages URL: \"not-a-uri-at-all\". All mapped_pages URLs must start with \"https://www.elastic.co/guide\". Please update the URL to reference content under the Elastic documentation guide." ); - } } diff --git a/tests/Elastic.Markdown.Tests/Inline/AnchorLinkTests.cs b/tests/Elastic.Markdown.Tests/Inline/AnchorLinkTests.cs index 215c42a63b..a87944016f 100644 --- a/tests/Elastic.Markdown.Tests/Inline/AnchorLinkTests.cs +++ b/tests/Elastic.Markdown.Tests/Inline/AnchorLinkTests.cs @@ -4,8 +4,11 @@ using System.IO.Abstractions.TestingHelpers; using AwesomeAssertions; +using Elastic.Documentation.Testing; using JetBrains.Annotations; using Markdig.Syntax.Inlines; +using Nullean.Snapshots; +using Nullean.Snapshots.Html; namespace Elastic.Markdown.Tests.Inline; @@ -54,10 +57,24 @@ public class InPageAnchorTests() : AnchorLinkTestBase(""" """) { [Test] - public void GeneratesHtml() => Html.ShouldContainHtml("""

Hello

"""); + public void GeneratesHtml() => + Html.Should().MatchInlineHtmlSnapshot( + """ +
+

+ Hello world +

+
+

A paragraph

+

+ Hello +

+ """, + DocsHtml.Body() + ); [Test] - public void HasNoErrors() => Collector.Diagnostics.Should().HaveCount(0); + public void HasNoErrors() => Collector.Diagnostics.ToSnapshotText().Should().MatchInlineSnapshot(""); } [InheritsTests] @@ -68,12 +85,23 @@ [Sub Requirements](testing/req.md#sub-requirements) [Test] public void GeneratesHtml() => // language=html - Html.ShouldContainHtml( - """

Sub Requirements

""" + Html.Should().MatchInlineHtmlSnapshot( + """ +
+

+ Hello world +

+
+

A paragraph

+

+ Sub Requirements +

+ """, + DocsHtml.Body() ); [Test] - public void HasNoErrors() => Collector.Diagnostics.Should().HaveCount(0); + public void HasNoErrors() => Collector.Diagnostics.ToSnapshotText().Should().MatchInlineSnapshot(""); } [InheritsTests] @@ -84,12 +112,23 @@ [Sub Requirements](testing/req.md#new-reqs) [Test] public void GeneratesHtml() => // language=html - Html.ShouldContainHtml( - """

Sub Requirements

""" + Html.Should().MatchInlineHtmlSnapshot( + """ +
+

+ Hello world +

+
+

A paragraph

+

+ Sub Requirements +

+ """, + DocsHtml.Body() ); [Test] - public void HasNoErrors() => Collector.Diagnostics.Should().HaveCount(0); + public void HasNoErrors() => Collector.Diagnostics.ToSnapshotText().Should().MatchInlineSnapshot(""); } [InheritsTests] @@ -99,12 +138,23 @@ public class ExternalPageAnchorAutoTitleTests() : AnchorLinkTestBase(""" { [Test] public void GeneratesHtml() => - Html.ShouldContainHtml( - """

Special Requirements > Sub Requirements

""" + Html.Should().MatchInlineHtmlSnapshot( + """ +
+

+ Hello world +

+
+

A paragraph

+

+ Special Requirements > Sub Requirements +

+ """, + DocsHtml.Body() ); [Test] - public void HasNoErrors() => Collector.Diagnostics.Should().HaveCount(0); + public void HasNoErrors() => Collector.Diagnostics.ToSnapshotText().Should().MatchInlineSnapshot(""); } [InheritsTests] @@ -113,11 +163,29 @@ public class InPageBadAnchorTests() : AnchorLinkTestBase(""" """) { [Test] - public void GeneratesHtml() => Html.ShouldContainHtml("""

Hello

"""); + public void GeneratesHtml() => + Html.Should().MatchInlineHtmlSnapshot( + """ +
+

+ Hello world +

+
+

A paragraph

+

+ Hello +

+ """, + DocsHtml.Body() + ); [Test] public void HasError() => - Collector.Diagnostics.Should().HaveCount(1).And.Contain(d => d.Message.Contains("`hello-world2` does not exist")); + Collector + .Diagnostics + .ToSnapshotText() + .Should() + .MatchInlineSnapshot("error {repo}/docs/index.md:7 `hello-world2` does not exist in index.md."); } [InheritsTests] @@ -127,13 +195,28 @@ [Sub Requirements](testing/req.md#sub-requirements2) { [Test] public void GeneratesHtml() => - Html.ShouldContainHtml( - """

Sub Requirements

""" + Html.Should().MatchInlineHtmlSnapshot( + """ +
+

+ Hello world +

+
+

A paragraph

+

+ Sub Requirements +

+ """, + DocsHtml.Body() ); [Test] public void HasError() => - Collector.Diagnostics.Should().HaveCount(1).And.Contain(d => d.Message.Contains("`sub-requirements2` does not exist")); + Collector + .Diagnostics + .ToSnapshotText() + .Should() + .MatchInlineSnapshot("error {repo}/docs/index.md:7 `sub-requirements2` does not exist in testing/req.md."); } [InheritsTests] @@ -143,11 +226,22 @@ [Heading inside dropdown](testing/req.md#heading-inside-dropdown) { [Test] public void GeneratesHtml() => - Html.ShouldContainHtml( - """Heading inside dropdown""" + Html.Should().MatchInlineHtmlSnapshot( + """ +
+

+ Hello world +

+
+

A paragraph

+

+ Heading inside dropdown +

+ """, + DocsHtml.Body() ); [Test] - public void HasError() => Collector.Diagnostics.Should().HaveCount(0); + public void HasError() => Collector.Diagnostics.ToSnapshotText().Should().MatchInlineSnapshot(""); } [InheritsTests] @@ -157,7 +251,13 @@ public class MissingMdExtensionTests() : AnchorLinkTestBase(""" { [Test] public void HasMdExtensionHintError() => - Collector.Diagnostics.Should().HaveCount(1).And.Contain(d => d.Message.Contains("Did you forget to add the .md extension?")); + Collector + .Diagnostics + .ToSnapshotText() + .Should() + .MatchInlineSnapshot( + "error {repo}/docs/index.md:7 `testing/req` is not a valid internal link. Did you forget to add the .md extension?" + ); } [InheritsTests] @@ -167,7 +267,13 @@ public class MissingMdExtensionWithAnchorTests() : AnchorLinkTestBase(""" { [Test] public void HasMdExtensionHintError() => - Collector.Diagnostics.Should().HaveCount(1).And.Contain(d => d.Message.Contains("Did you forget to add the .md extension?")); + Collector + .Diagnostics + .ToSnapshotText() + .Should() + .MatchInlineSnapshot( + "error {repo}/docs/index.md:7 `testing/req` is not a valid internal link. Did you forget to add the .md extension?" + ); } [InheritsTests] @@ -179,10 +285,9 @@ public class MissingFileNoMdHintTests() : AnchorLinkTestBase(""" public void HasGenericNotFoundError() => Collector .Diagnostics + .ToSnapshotText() .Should() - .HaveCount(1) - .And - .Contain(d => d.Message.Contains("does not exist")) - .And - .NotContain(d => d.Message.Contains("Did you forget to add the .md extension?")); + .MatchInlineSnapshot( + "error {repo}/docs/index.md:7 `testing/nonexistent` does not exist. If it was recently removed add a redirect. resolved to `{repo}/docs/testing/nonexistent`" + ); } diff --git a/tests/Elastic.Markdown.Tests/Inline/AutoLinkTests.cs b/tests/Elastic.Markdown.Tests/Inline/AutoLinkTests.cs index de40af1a39..251e3c087a 100644 --- a/tests/Elastic.Markdown.Tests/Inline/AutoLinkTests.cs +++ b/tests/Elastic.Markdown.Tests/Inline/AutoLinkTests.cs @@ -4,8 +4,11 @@ using AwesomeAssertions; using Elastic.Documentation.Diagnostics; +using Elastic.Documentation.Testing; using JetBrains.Annotations; using Markdig.Syntax.Inlines; +using Nullean.Snapshots; +using Nullean.Snapshots.Html; namespace Elastic.Markdown.Tests.Inline; @@ -33,10 +36,17 @@ public class BasicAutoLinkTests() : AutoLinkTestBase(""" { [Test] public void GeneratesHtml() => - Html.Should().Contain("""https://docs.test.io"""); + Html.Should().MatchInlineHtmlSnapshot( + """ +

Check out + https://docs.test.io + for more info.

+ """, + DocsHtml.Body() + ); [Test] - public void HasNoErrors() => Collector.Diagnostics.Should().HaveCount(0); + public void HasNoErrors() => Collector.Diagnostics.ToSnapshotText().Should().MatchInlineSnapshot(""); } [InheritsTests] @@ -46,12 +56,17 @@ public class AutoLinkWithPathTests() : AutoLinkTestBase(""" { [Test] public void GeneratesHtml() => - Html.Should().Contain( - """https://docs.test.io/path/to/page""" + Html.Should().MatchInlineHtmlSnapshot( + """ +

Visit + https://docs.test.io/path/to/page + for details.

+ """, + DocsHtml.Body() ); [Test] - public void HasNoErrors() => Collector.Diagnostics.Should().HaveCount(0); + public void HasNoErrors() => Collector.Diagnostics.ToSnapshotText().Should().MatchInlineSnapshot(""); } [InheritsTests] @@ -61,12 +76,17 @@ public class AutoLinkWithQueryStringTests() : AutoLinkTestBase(""" { [Test] public void GeneratesHtml() => - Html.Should().Contain( - """https://docs.test.io/search?q=test&page=1""" + Html.Should().MatchInlineHtmlSnapshot( + """ +

See + https://docs.test.io/search?q=test&page=1 + for results.

+ """, + DocsHtml.Body() ); [Test] - public void HasNoErrors() => Collector.Diagnostics.Should().HaveCount(0); + public void HasNoErrors() => Collector.Diagnostics.ToSnapshotText().Should().MatchInlineSnapshot(""); } [InheritsTests] @@ -76,12 +96,17 @@ public class AutoLinkWithAnchorTests() : AutoLinkTestBase(""" { [Test] public void GeneratesHtml() => - Html.Should().Contain( - """https://docs.test.io/page#section""" + Html.Should().MatchInlineHtmlSnapshot( + """ +

Jump to + https://docs.test.io/page#section + for the section.

+ """, + DocsHtml.Body() ); [Test] - public void HasNoErrors() => Collector.Diagnostics.Should().HaveCount(0); + public void HasNoErrors() => Collector.Diagnostics.ToSnapshotText().Should().MatchInlineSnapshot(""); } [InheritsTests] @@ -91,10 +116,16 @@ public class AutoLinkTrailingPeriodTests() : AutoLinkTestBase(""" { [Test] public void ExcludesTrailingPeriod() => - Html.Should().Contain("""https://docs.test.io."""); + Html.Should().MatchInlineHtmlSnapshot( + """ +

Check out + https://docs.test.io.

+ """, + DocsHtml.Body() + ); [Test] - public void HasNoErrors() => Collector.Diagnostics.Should().HaveCount(0); + public void HasNoErrors() => Collector.Diagnostics.ToSnapshotText().Should().MatchInlineSnapshot(""); } [InheritsTests] @@ -106,16 +137,19 @@ public class AutoLinkTrailingCommaTests() : AutoLinkTestBase( { [Test] public void ExcludesTrailingCommas() => - Html - .Should() - .Contain("""https://first.test.io,""") - .And - .Contain("""https://second.test.io,""") - .And - .Contain("""https://third.test.io"""); + Html.Should().MatchInlineHtmlSnapshot( + """ +

Visit + https://first.test.io, + https://second.test.io, or + https://third.test.io + for info.

+ """, + DocsHtml.Body() + ); [Test] - public void HasNoErrors() => Collector.Diagnostics.Should().HaveCount(0); + public void HasNoErrors() => Collector.Diagnostics.ToSnapshotText().Should().MatchInlineSnapshot(""); } [InheritsTests] @@ -125,10 +159,13 @@ public class AutoLinkInParenthesesTests() : AutoLinkTestBase(""" { [Test] public void ExcludesClosingParen() => - Html.Should().Contain("""(https://docs.test.io)"""); + Html.Should().MatchInlineHtmlSnapshot( + "

See the docs (https://docs.test.io) for details.

", + DocsHtml.Body() + ); [Test] - public void HasNoErrors() => Collector.Diagnostics.Should().HaveCount(0); + public void HasNoErrors() => Collector.Diagnostics.ToSnapshotText().Should().MatchInlineSnapshot(""); } [InheritsTests] @@ -140,12 +177,17 @@ public class AutoLinkWithBalancedParensTests() : AutoLinkTestBase( { [Test] public void IncludesBalancedParens() => - Html.Should().Contain( - """https://en.wikipedia.org/wiki/Rust_(programming_language)""" + Html.Should().MatchInlineHtmlSnapshot( + """ +

Check + https://en.wikipedia.org/wiki/Rust_(programming_language) + for more.

+ """, + DocsHtml.Body() ); [Test] - public void HasNoErrors() => Collector.Diagnostics.Should().HaveCount(0); + public void HasNoErrors() => Collector.Diagnostics.ToSnapshotText().Should().MatchInlineSnapshot(""); } [InheritsTests] @@ -155,19 +197,23 @@ public class AutoLinkElasticDocsHintTests() : AutoLinkTestBase(""" { [Test] public void GeneratesHtml() => - Html.Should().Contain( - """https://www.elastic.co/docs/deploy-manage""" + Html.Should().MatchInlineHtmlSnapshot( + """ +

See + https://www.elastic.co/docs/deploy-manage + for deployment info.

+ """, + DocsHtml.Body() ); [Test] public void EmitsHint() => Collector .Diagnostics + .ToSnapshotText() .Should() - .ContainSingle( - d => d.Severity == Severity.Hint && d.Message.Contains("elastic.co/docs") && d.Message.Contains( - "crosslink or relative link" - ) + .MatchInlineSnapshot( + "hint {repo}/docs/index.md:3 Autolink points to elastic.co/docs. Consider using a crosslink or relative link instead." ); } @@ -178,10 +224,23 @@ public class AutoLinkInCodeBlockTests() : AutoLinkNotFoundTestBase(""" """) { [Test] - public void DoesNotCreateLink() => Html.Should().NotContain(" +
+
+					https://docs.test.io/should/not/be/linked
+					
+				
+
+ + """, + DocsHtml.Body() + ); [Test] - public void HasNoErrors() => Collector.Diagnostics.Should().HaveCount(0); + public void HasNoErrors() => Collector.Diagnostics.ToSnapshotText().Should().MatchInlineSnapshot(""); } public class AutoLinkInInlineCodeTests() : AutoLinkNotFoundTestBase(""" @@ -190,10 +249,17 @@ public class AutoLinkInInlineCodeTests() : AutoLinkNotFoundTestBase(""" { [Test] public void DoesNotCreateLinkInInlineCode() => - Html.Should().Contain("https://docs.test.io/api").And.NotContain("""
Use the URL + https://docs.test.io/api + in your config.

+ """, + DocsHtml.Body() + ); [Test] - public void HasNoErrors() => Collector.Diagnostics.Should().HaveCount(0); + public void HasNoErrors() => Collector.Diagnostics.ToSnapshotText().Should().MatchInlineSnapshot(""); } public class AutoLinkDoesNotMatchHttpTests() : AutoLinkNotFoundTestBase(""" @@ -201,10 +267,11 @@ public class AutoLinkDoesNotMatchHttpTests() : AutoLinkNotFoundTestBase(""" """) { [Test] - public void DoesNotCreateLink() => Html.Should().NotContain("
This http://docs.test.io should not be autolinked.

", DocsHtml.Body()); [Test] - public void HasNoErrors() => Collector.Diagnostics.Should().HaveCount(0); + public void HasNoErrors() => Collector.Diagnostics.ToSnapshotText().Should().MatchInlineSnapshot(""); } [InheritsTests] @@ -216,14 +283,19 @@ public class AutoLinkWithStandardLinkTests() : AutoLinkTestBase( { [Test] public void BothLinksWork() => - Html - .Should() - .Contain("""
Docs""") - .And - .Contain("""https://other.test.io"""); + Html.Should().MatchInlineHtmlSnapshot( + """ +

Visit + Docs + or + https://other.test.io + for more.

+ """, + DocsHtml.Body() + ); [Test] - public void HasNoErrors() => Collector.Diagnostics.Should().HaveCount(0); + public void HasNoErrors() => Collector.Diagnostics.ToSnapshotText().Should().MatchInlineSnapshot(""); } [InheritsTests] @@ -235,15 +307,21 @@ public class AutoLinkInsideLinkTextTests() : AutoLinkTestBase( ) { [Test] - public void DoesNotCreateNestedAnchor() => - Html - .Should() - .Contain("""https://gist.github.com""") - .And - .NotMatchRegex(@"]*>Upload to a service like + https://gist.github.com.

+ """, + DocsHtml.Body() + ); + // The HTML parser would split nested anchors in the snapshot, so look for them in the raw output. + Html.Should().NotMatchRegex(@"]*> Collector.Diagnostics.Should().HaveCount(0); + public void HasNoErrors() => Collector.Diagnostics.ToSnapshotText().Should().MatchInlineSnapshot(""); } [InheritsTests] @@ -255,12 +333,16 @@ public class AutoLinkInsideLinkTextWithSurroundingTextTests() : AutoLinkTestBase { [Test] public void DoesNotAutolinkUrlInsideLinkText() => - Html.Should().Contain( - """the page at https://example.test.io for details""" + Html.Should().MatchInlineHtmlSnapshot( + """ +

See + the page at https://example.test.io for details.

+ """, + DocsHtml.Body() ); [Test] - public void HasNoErrors() => Collector.Diagnostics.Should().HaveCount(0); + public void HasNoErrors() => Collector.Diagnostics.ToSnapshotText().Should().MatchInlineSnapshot(""); } // Verify that image-inside-link is unaffected by the IsNestedInsideLink guard (images bypass it via the IsImage branch). @@ -270,14 +352,20 @@ public class ImageInsideLinkTests() : InlineTest(""" """) { [Test] - public void RendersOuterAnchor() => - Html.Should().Contain(""""""); - - [Test] - public void RendersImage() => Html.Should().Contain(" + Html.Should().MatchInlineHtmlSnapshot( + """ +

+ + alt text + +

+ """, + DocsHtml.Body() + ); [Test] - public void HasNoErrors() => Collector.Diagnostics.Should().HaveCount(0); + public void HasNoErrors() => Collector.Diagnostics.ToSnapshotText().Should().MatchInlineSnapshot(""); } [InheritsTests] @@ -289,16 +377,21 @@ public class MultipleAutoLinksTests() : AutoLinkTestBase( { [Test] public void AllLinksAreCreated() => - Html - .Should() - .Contain("""First + https://first.com + then + https://second.com + and finally + https://third.com + are all linked.

+ """, + DocsHtml.Body() + ); [Test] - public void HasNoErrors() => Collector.Diagnostics.Should().HaveCount(0); + public void HasNoErrors() => Collector.Diagnostics.ToSnapshotText().Should().MatchInlineSnapshot(""); } // === Exclusion rule tests === @@ -308,10 +401,11 @@ public class AutoLinkWithPortExclusionTests() : AutoLinkNotFoundTestBase(""" """) { [Test] - public void DoesNotCreateLinkForUrlWithPort() => Html.Should().NotContain(" + Html.Should().MatchInlineHtmlSnapshot("

Connect to https://www.elastic.co:443/guide for the guide.

", DocsHtml.Body()); [Test] - public void HasNoErrors() => Collector.Diagnostics.Should().HaveCount(0); + public void HasNoErrors() => Collector.Diagnostics.ToSnapshotText().Should().MatchInlineSnapshot(""); } public class AutoLinkLocalhostExclusionTests() : AutoLinkNotFoundTestBase(""" @@ -319,10 +413,11 @@ public class AutoLinkLocalhostExclusionTests() : AutoLinkNotFoundTestBase(""" """) { [Test] - public void DoesNotCreateLinkForLocalhost() => Html.Should().NotContain("
+ Html.Should().MatchInlineHtmlSnapshot("

Check https://localhost/api for the local API.

", DocsHtml.Body()); [Test] - public void HasNoErrors() => Collector.Diagnostics.Should().HaveCount(0); + public void HasNoErrors() => Collector.Diagnostics.ToSnapshotText().Should().MatchInlineSnapshot(""); } public class AutoLinkLoopbackExclusionTests() : AutoLinkNotFoundTestBase(""" @@ -330,10 +425,11 @@ public class AutoLinkLoopbackExclusionTests() : AutoLinkNotFoundTestBase(""" """) { [Test] - public void DoesNotCreateLinkForLoopback() => Html.Should().NotContain("
+ Html.Should().MatchInlineHtmlSnapshot("

Test at https://127.0.0.1/health for health check.

", DocsHtml.Body()); [Test] - public void HasNoErrors() => Collector.Diagnostics.Should().HaveCount(0); + public void HasNoErrors() => Collector.Diagnostics.ToSnapshotText().Should().MatchInlineSnapshot(""); } public class AutoLinkExampleDomainExclusionTests() : AutoLinkNotFoundTestBase(""" @@ -341,10 +437,11 @@ public class AutoLinkExampleDomainExclusionTests() : AutoLinkNotFoundTestBase("" """) { [Test] - public void DoesNotCreateLinkForExampleDomain() => Html.Should().NotContain("
+ Html.Should().MatchInlineHtmlSnapshot("

See https://example.com/docs for examples.

", DocsHtml.Body()); [Test] - public void HasNoErrors() => Collector.Diagnostics.Should().HaveCount(0); + public void HasNoErrors() => Collector.Diagnostics.ToSnapshotText().Should().MatchInlineSnapshot(""); } public class AutoLinkExampleSubdomainExclusionTests() : AutoLinkNotFoundTestBase(""" @@ -353,10 +450,10 @@ public class AutoLinkExampleSubdomainExclusionTests() : AutoLinkNotFoundTestBase { [Test] public void DoesNotCreateLinkForExampleSubdomain() => - Html.Should().NotContain("
Visit https://system.example.com/setup for setup.

", DocsHtml.Body()); [Test] - public void HasNoErrors() => Collector.Diagnostics.Should().HaveCount(0); + public void HasNoErrors() => Collector.Diagnostics.ToSnapshotText().Should().MatchInlineSnapshot(""); } public class AutoLinkTemplatePlaceholderExclusionTests() : AutoLinkNotFoundTestBase( @@ -366,7 +463,8 @@ public class AutoLinkTemplatePlaceholderExclusionTests() : AutoLinkNotFoundTestB ) { [Test] - public void DoesNotCreateLinkForTemplatePlaceholder() => Html.Should().NotContain("
+ Html.Should().MatchInlineHtmlSnapshot("

Use https://{{cluster_id}}.es.test.co/api for your cluster.

", DocsHtml.Body()); // Note: We expect an error because {{cluster_id}} is an undefined substitution key, // but the important assertion is that the URL is not autolinked. @@ -379,10 +477,11 @@ public class AutoLinkAsciiDocStyleExclusionTests() : AutoLinkNotFoundTestBase( ) { [Test] - public void DoesNotCreateLinkForAsciiDocStyle() => Html.Should().NotContain("""
+ Html.Should().MatchInlineHtmlSnapshot("

See https://www.iana.org/assignments[IANA for assignments.

", DocsHtml.Body()); [Test] - public void HasNoErrors() => Collector.Diagnostics.Should().HaveCount(0); + public void HasNoErrors() => Collector.Diagnostics.ToSnapshotText().Should().MatchInlineSnapshot(""); } [InheritsTests] @@ -392,10 +491,15 @@ public class AutoLinkValidUrlStillWorksTests() : AutoLinkTestBase(""" { [Test] public void CreatesLinkForValidUrl() => - Html.Should().Contain( - """
https://www.elastic.co/guide""" + Html.Should().MatchInlineHtmlSnapshot( + """ +

Check + https://www.elastic.co/guide + for docs.

+ """, + DocsHtml.Body() ); [Test] - public void HasNoErrors() => Collector.Diagnostics.Should().HaveCount(0); + public void HasNoErrors() => Collector.Diagnostics.ToSnapshotText().Should().MatchInlineSnapshot(""); } diff --git a/tests/Elastic.Markdown.Tests/Inline/CommentTest.cs b/tests/Elastic.Markdown.Tests/Inline/CommentTest.cs index f55dbed559..897a6be0a4 100644 --- a/tests/Elastic.Markdown.Tests/Inline/CommentTest.cs +++ b/tests/Elastic.Markdown.Tests/Inline/CommentTest.cs @@ -2,6 +2,9 @@ // 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 Elastic.Documentation.Testing; +using Nullean.Snapshots; +using Nullean.Snapshots.Html; namespace Elastic.Markdown.Tests.Inline; @@ -11,14 +14,10 @@ not a comment """) { [Test] - public void GeneratesAttributesInHtml() - { - // language=html - Html.Should().NotContain("""

% comment""").And.Contain("""

not a comment

"""); - Html.ShouldBeHtml(""" + public void GeneratesAttributesInHtml() => + Html.Should().MatchInlineHtmlSnapshot("""

not a comment

- """); - } + """, DocsHtml.Body()); } public class MultipleLineCommentTest() : InlineTest( @@ -35,27 +34,14 @@ also not a comment ) { [Test] - public void GeneratesAttributesInHtml() - { - // language=html - Html - .Should() - .NotContainAny( - "

" - ) - .And - .ContainAll("

not a comment, and multi line comment below

", "

also not a comment

"); - Html.ShouldBeHtml( + public void GeneratesAttributesInHtml() => + Html.Should().MatchInlineHtmlSnapshot( """

not a comment, and multi line comment below

also not a comment

- """ + """, + DocsHtml.Body() ); - } } public class MultipleLineCommentWithLinkTest() : InlineTest( @@ -73,32 +59,17 @@ also not a comment ) { [Test] - public void HasNoErrors() => Collector.Diagnostics.Should().HaveCount(0); + public void HasNoErrors() => Collector.Diagnostics.ToSnapshotText().Should().MatchInlineSnapshot(""); [Test] - public void GeneratesAttributesInHtml() - { - // language=html - Html - .ReplaceLineEndings() - .Should() - .NotContainAny( - "

" - ) - .And - .ContainAll("

not a comment, and multi line comment below

", "

also not a comment

"); - Html.ShouldBeHtml( + public void GeneratesAttributesInHtml() => + Html.Should().MatchInlineHtmlSnapshot( """

not a comment, and multi line comment below

also not a comment

- """ + """, + DocsHtml.Body() ); - } } /// @@ -119,18 +90,16 @@ content after comment ) { [Test] - public void ContentAfterCommentShouldBeRendered() - { - // This test verifies GitHub issue #2456 is fixed. - // The "content after comment" should be rendered, not silently dropped. - Html.Should().Contain("

content after comment

"); - } - - [Test] - public void ContentBeforeCommentShouldBeRendered() => Html.Should().Contain("

content before comment

"); - - [Test] - public void CommentContentShouldNotBeRendered() => Html.Should().NotContain("TODO: Uncomment once page is live."); + // This test verifies GitHub issue #2456 is fixed. + // The "content after comment" should be rendered, not silently dropped. + public void GeneratesHtml() => + Html.Should().MatchInlineHtmlSnapshot( + """ +

content before comment

+

content after comment

+ """, + DocsHtml.Body() + ); } /// @@ -145,11 +114,11 @@ content after """) { [Test] - public void ContentBeforeAndAfterShouldBeRendered() => - Html.Should().Contain("

content before

").And.Contain("

content after

"); - - [Test] - public void CommentContentShouldNotBeRendered() => Html.Should().NotContain("single line comment"); + public void GeneratesHtml() => + Html.Should().MatchInlineHtmlSnapshot(""" +

content before

+

content after

+ """, DocsHtml.Body()); } /// @@ -168,10 +137,9 @@ content after ) { [Test] - public void ContentBeforeAndAfterShouldBeRendered() => - Html.Should().Contain("

content before

").And.Contain("

content after

"); - - [Test] - public void CommentContentShouldNotBeRendered() => - Html.Should().NotContain("start of comment").And.NotContain("middle of comment").And.NotContain("end of comment"); + public void GeneratesHtml() => + Html.Should().MatchInlineHtmlSnapshot(""" +

content before

+

content after

+ """, DocsHtml.Body()); } diff --git a/tests/Elastic.Markdown.Tests/Inline/DirectiveBlockLinkTests.cs b/tests/Elastic.Markdown.Tests/Inline/DirectiveBlockLinkTests.cs index 326c4f4a53..39bab26f60 100644 --- a/tests/Elastic.Markdown.Tests/Inline/DirectiveBlockLinkTests.cs +++ b/tests/Elastic.Markdown.Tests/Inline/DirectiveBlockLinkTests.cs @@ -4,8 +4,11 @@ using System.IO.Abstractions.TestingHelpers; using AwesomeAssertions; +using Elastic.Documentation.Testing; using JetBrains.Annotations; using Markdig.Syntax.Inlines; +using Nullean.Snapshots; +using Nullean.Snapshots.Html; namespace Elastic.Markdown.Tests.Inline; @@ -46,10 +49,25 @@ public class InPageDirectiveLinkTests() : DirectiveBlockLinkTests(""" [Test] public void GeneratesHtml() => // language=html - Html.ShouldContainHtml("""

Hello

"""); + Html.Should().MatchInlineHtmlSnapshot( + """ +
+
+ Warning +
+
+

This is a 'warning' admonition

+
+
+

+ Hello +

+ """, + DocsHtml.Body() + ); [Test] - public void HasNoErrors() => Collector.Diagnostics.Should().HaveCount(0); + public void HasNoErrors() => Collector.Diagnostics.ToSnapshotText().Should().MatchInlineSnapshot(""); } [InheritsTests] @@ -58,8 +76,24 @@ [Sub Requirements](testing/req.md#hint_ref) """) { [Test] - public void GeneratesHtml() => Html.ShouldContainHtml("""

Sub Requirements

"""); + public void GeneratesHtml() => + Html.Should().MatchInlineHtmlSnapshot( + """ +
+
+ Warning +
+
+

This is a 'warning' admonition

+
+
+

+ Sub Requirements +

+ """, + DocsHtml.Body() + ); [Test] - public void HasNoErrors() => Collector.Diagnostics.Should().HaveCount(0); + public void HasNoErrors() => Collector.Diagnostics.ToSnapshotText().Should().MatchInlineSnapshot(""); } diff --git a/tests/Elastic.Markdown.Tests/Inline/FootnotesTests.cs b/tests/Elastic.Markdown.Tests/Inline/FootnotesTests.cs index d188d15864..8541dccf98 100644 --- a/tests/Elastic.Markdown.Tests/Inline/FootnotesTests.cs +++ b/tests/Elastic.Markdown.Tests/Inline/FootnotesTests.cs @@ -3,7 +3,9 @@ // See the LICENSE file in the project root for more information using AwesomeAssertions; +using Elastic.Documentation.Testing; using JetBrains.Annotations; +using Nullean.Snapshots.Html; namespace Elastic.Markdown.Tests.Inline; @@ -18,35 +20,32 @@ public class FootnotesBasicTests() : InlineTest( ) { [Test] - public void ContainsFootnoteReferences() - { - Html.Should().Contain("footnote-ref"); - Html.Should().Contain("href=\"#fn:1\""); - Html.Should().Contain("href=\"#fn:2\""); - } - - [Test] - public void ContainsFootnoteContainer() => Html.Should().Contain("class=\"footnotes\""); - - [Test] - public void ContainsFootnoteDefinitions() - { - Html.Should().Contain("id=\"fn:1\""); - Html.Should().Contain("id=\"fn:2\""); - Html.Should().Contain("This is the first footnote."); - Html.Should().Contain("This is the second footnote."); - } - - [Test] - public void ContainsBackReferences() - { - Html.Should().Contain("footnote-back-ref"); - Html.Should().Contain("href=\"#fnref:1\""); - Html.Should().Contain("href=\"#fnref:2\""); - } - - [Test] - public void RendersFootnotesHeading() => Html.Should().Contain("

Footnotes

"); + public void GeneratesHtml() => + Html.Should().MatchInlineHtmlSnapshot( + """ +

Here's a simple footnote + 1 + + and another + 2 + .

+
+
+

Footnotes

+
    +
  1. +

    This is the first footnote.↩ +

    +
  2. +
  3. +

    This is the second footnote.↩ +

    +
  4. +
+
+ """, + DocsHtml.Body() + ); [Test] public void FootnotesHeadingPrecedesFootnoteContainer() @@ -83,12 +82,30 @@ public void ContainsMultipleReferencesToSameFootnote() } [Test] - public void ContainsMultipleBackReferences() - { - // Should have references back to both instances - Html.Should().Contain("href=\"#fnref:1\""); - Html.Should().Contain("href=\"#fnref:2\""); - } + // Should have references back to both instances + public void ContainsMultipleBackReferences() => + Html.Should().MatchInlineHtmlSnapshot( + """ +

First reference + 1 + + and second reference + 1 + .

+
+
+

Footnotes

+
    +
  1. +

    This footnote is referenced twice.↩ + ↩ +

    +
  2. +
+
+ """, + DocsHtml.Body() + ); [System.Text.RegularExpressions.GeneratedRegex("href=\"#fn:1\"")] private static partial System.Text.RegularExpressions.Regex MyRegex(); @@ -111,26 +128,35 @@ It has multiple paragraphs. ) { [Test] - public void ContainsComplexFootnoteStructure() - { - Html.Should().Contain("href=\"#fn:1\""); - Html.Should().Contain("This footnote has multiple elements."); - Html.Should().Contain("It has multiple paragraphs."); - } - - [Test] - public void ContainsBlockquoteInFootnote() - { - Html.Should().Contain("blockquote"); - Html.Should().Contain("And even a blockquote."); - } - - [Test] - public void ContainsListInFootnote() - { - Html.Should().Contain("List item 1"); - Html.Should().Contain("List item 2"); - } + public void GeneratesHtml() => + Html.Should().MatchInlineHtmlSnapshot( + """ +

Here's a complex footnote + 1 + .

+
+
+

Footnotes

+
    +
  1. +

    This footnote has multiple elements.

    +

    It has multiple paragraphs.

    +
    +

    And even a blockquote.

    +
    +
      +
    • List item 1
    • +
    • List item 2
    • +
    +

    + ↩ +

    +
  2. +
+
+ """, + DocsHtml.Body() + ); } public class FootnotesWithCodeTests() : InlineTest( @@ -148,11 +174,35 @@ def hello(): ) { [Test] - public void ContainsCodeBlockInFootnote() - { - Html.Should().Contain("Example code:"); - Html.Should().Contain("hello"); - } + public void ContainsCodeBlockInFootnote() => + Html.Should().MatchInlineHtmlSnapshot( + """ +

See the code example + 1 + .

+
+
+

Footnotes

+
    +
  1. +

    Example code:

    +
    +
    +
    +								def hello():     print("Hello, world!")
    +								
    +							
    +
    +
    +

    + ↩ +

    +
  2. +
+
+ """, + DocsHtml.Body() + ); } public class FootnotesConsecutiveDefinitionsTests() : InlineTest( @@ -167,20 +217,7 @@ public class FootnotesConsecutiveDefinitionsTests() : InlineTest( ) { [Test] - public void HandlesConsecutiveFootnoteDefinitions() - { - Html.Should().Contain("First footnote."); - Html.Should().Contain("Second footnote."); - Html.Should().Contain("Third footnote."); - } - - [Test] - public void AllFootnoteReferencesAreLinked() - { - Html.Should().Contain("href=\"#fn:1\""); - Html.Should().Contain("href=\"#fn:2\""); - Html.Should().Contain("href=\"#fn:3\""); - } + public void GeneratesHtml() => Html.Should().MatchHtmlSnapshot(DocsHtml.Body()); } public class FootnotesInListTests() : InlineTest( @@ -194,11 +231,29 @@ public class FootnotesInListTests() : InlineTest( ) { [Test] - public void FootnoteWorksInListItem() - { - Html.Should().Contain("href=\"#fn:1\""); - Html.Should().Contain("Footnote from list item."); - } + public void FootnoteWorksInListItem() => + Html.Should().MatchInlineHtmlSnapshot( + """ +
    +
  • Item one
  • +
  • Item two with footnote + 1 + +
  • +
+
+
+

Footnotes

+
    +
  1. +

    Footnote from list item.↩ +

    +
  2. +
+
+ """, + DocsHtml.Body() + ); } public class FootnotesWithNamedReferencesTests() : InlineTest( @@ -211,11 +266,25 @@ Named reference[^my-footnote]. ) { [Test] - public void HandlesNamedFootnoteIdentifiers() - { - Html.Should().Contain("footnote-ref"); - Html.Should().Contain("This uses a named identifier."); - } + public void HandlesNamedFootnoteIdentifiers() => + Html.Should().MatchInlineHtmlSnapshot( + """ +

Named reference + 1 + .

+
+
+

Footnotes

+
    +
  1. +

    This uses a named identifier.↩ +

    +
  2. +
+
+ """, + DocsHtml.Body() + ); } public partial class FootnotesInlineCodeNotParsedTests() : InlineTest( @@ -228,11 +297,28 @@ public partial class FootnotesInlineCodeNotParsedTests() : InlineTest( ) { [Test] - public void InlineCodeFootnoteSyntaxNotParsed() - { - // The inline code `[^1]` should render as code, not as a footnote reference - Html.Should().Contain("[^1]"); - } + // The inline code `[^1]` should render as code, not as a footnote reference + public void InlineCodeFootnoteSyntaxNotParsed() => + Html.Should().MatchInlineHtmlSnapshot( + """ +

Real reference + 1 + . Inline code example: + [^1] + should not be parsed.

+
+
+

Footnotes

+
    +
  1. +

    This is the footnote.↩ +

    +
  2. +
+
+ """, + DocsHtml.Body() + ); [Test] public void OnlyOneBackReference() @@ -260,7 +346,33 @@ Code block with [^1] reference. ) { [Test] - public void CodeBlockRendered() => Html.Should().Contain("language-markdown"); + public void CodeBlockRendered() => + Html.Should().MatchInlineHtmlSnapshot( + """ +

Real reference + 1 + .

+
+
+
+					Code block with [^1] reference.
+					
+				
+
+
+
+
+

Footnotes

+
    +
  1. +

    This is the footnote.↩ +

    +
  2. +
+
+ """, + DocsHtml.Body() + ); [Test] public void OnlyOneBackReference() @@ -304,7 +416,7 @@ public partial class FootnotesCodeBlockInDirectiveTests() : InlineTest( ) { [Test] - public void CodeBlockRendered() => Html.Should().Contain("language-markdown"); + public void CodeBlockRendered() => Html.Should().MatchHtmlSnapshot(DocsHtml.Body()); [Test] public void CorrectBackReferenceCount() @@ -347,26 +459,8 @@ public class FootnotesInsideDirectiveTests() : InlineTest( ) { [Test] - public void OtherInlineElementsWorkInsideDirectives() - { - // Do other inline elements work? - Html.Should().Contain("bold"); - Html.Should().Contain("href=\"https://example.com\""); - } - - [Test] - public void FootnoteReferencesWorkInsideDirectives() - { - // Footnote REFERENCES work inside directives - Html.Should().Contain("footnote-ref"); - Html.Should().Contain("href=\"#fn:1\""); - } - - [Test] - public void FootnoteDefinitionsAreAtDocumentLevel() - { - // Footnote DEFINITIONS are rendered at the document level - Html.Should().Contain("
"); - Html.Should().Contain("Footnote definitions must be at the document level"); - } + // Do other inline elements work? + // Footnote REFERENCES work inside directives + // Footnote DEFINITIONS are rendered at the document level + public void GeneratesHtml() => Html.Should().MatchHtmlSnapshot(DocsHtml.Body()); } diff --git a/tests/Elastic.Markdown.Tests/Inline/HardBreakTests.cs b/tests/Elastic.Markdown.Tests/Inline/HardBreakTests.cs index 506d6aea65..4b2882bb60 100644 --- a/tests/Elastic.Markdown.Tests/Inline/HardBreakTests.cs +++ b/tests/Elastic.Markdown.Tests/Inline/HardBreakTests.cs @@ -2,23 +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 AwesomeAssertions; +using Elastic.Documentation.Testing; +using Nullean.Snapshots.Html; + namespace Elastic.Markdown.Tests.Inline; public class AllowBrTagTest() : InlineTest("Hello,
World!") { [Test] - public void GeneratesHtml() => Html.ShouldContainHtml("

Hello,
World!

"); + public void GeneratesHtml() => Html.Should().MatchInlineHtmlSnapshot("

Hello,
World!

", DocsHtml.Body()); } public class BrTagNeedsToBeExact() : InlineTest("Hello,
World
!") { [Test] - public void GeneratesHtml() => Html.ShouldContainHtml("

Hello,<br >World<br />!

"); + public void GeneratesHtml() => Html.Should().MatchInlineHtmlSnapshot("

Hello,<br >World<br />!

", DocsHtml.Body()); } public class DisallowSpanTag() : InlineTest("Hello,World!") { [Test] // span tag is rendered as text - public void GeneratesHtml() => Html.ShouldContainHtml("

Hello,<span>World!</span>

"); + public void GeneratesHtml() => Html.Should().MatchInlineHtmlSnapshot("

Hello,<span>World!</span>

", DocsHtml.Body()); } diff --git a/tests/Elastic.Markdown.Tests/Inline/IconParserTests.cs b/tests/Elastic.Markdown.Tests/Inline/IconParserTests.cs index e729a80290..51ee2fd8d2 100644 --- a/tests/Elastic.Markdown.Tests/Inline/IconParserTests.cs +++ b/tests/Elastic.Markdown.Tests/Inline/IconParserTests.cs @@ -3,6 +3,8 @@ // See the LICENSE file in the project root for more information using AwesomeAssertions; +using Elastic.Documentation.Testing; +using Nullean.Snapshots.Html; namespace Elastic.Markdown.Tests.Inline; @@ -19,25 +21,32 @@ An unknown icon {icon}`not_a_real_icon` should not be replaced. { [Test] public void Render() => - Html - .Should() - .Contain("") - .And - .Contain("") - .And - .Contain("") - .And - .NotContain("{icon}`check`") - .And - .NotContain("{icon}`cross`") - .And - .NotContain("{icon}`warning`") - .And - .Contain("/this:apm_trace:is:not:an:icon") - .And - .Contain(":invalid-icon:") - .And - .Contain("::"); + Html.Should().MatchInlineHtmlSnapshot( + """ +

A check mark + + + + + . A cross + + + + + . A warning + + + + + + + .

+

An unknown icon + + should not be replaced. This should not be an icon either/this:apm_trace:is:not:an:icon. Nor should this be an icon :invalid-icon:. An empty one is not an icon either ::.

+ """, + DocsHtml.Body() + ); } public class IconInListItemTest() : InlineTest(""" @@ -46,13 +55,20 @@ public class IconInListItemTest() : InlineTest(""" { [Test] public void Render() => - Html - .Should() - .Contain("") - .And - .NotContain("{icon}`check`") - .And - .NotContain("
  • "); + Html.Should().MatchInlineHtmlSnapshot( + """ +
      +
    • + + + + + + A check mark.
    • +
    + """, + DocsHtml.Body() + ); } public class IconInHeadingShouldBeRemovedFromAnchor() : InlineTest(""" @@ -61,11 +77,20 @@ public class IconInHeadingShouldBeRemovedFromAnchor() : InlineTest(""" { [Test] public void Render() => - Html - .Should() - .Contain("") - .And - .Contain("Users +

    + Users + + + + + + +

    +
    + """, + DocsHtml.Body() + ); } diff --git a/tests/Elastic.Markdown.Tests/Inline/InlineAnchorTests.cs b/tests/Elastic.Markdown.Tests/Inline/InlineAnchorTests.cs index daf95f63f6..d9863d7d55 100644 --- a/tests/Elastic.Markdown.Tests/Inline/InlineAnchorTests.cs +++ b/tests/Elastic.Markdown.Tests/Inline/InlineAnchorTests.cs @@ -4,10 +4,13 @@ using System.IO.Abstractions.TestingHelpers; using AwesomeAssertions; +using Elastic.Documentation.Testing; using Elastic.Markdown.Myst.InlineParsers; using JetBrains.Annotations; using Markdig.Syntax; using Markdig.Syntax.Inlines; +using Nullean.Snapshots; +using Nullean.Snapshots.Html; namespace Elastic.Markdown.Tests.Inline; @@ -27,8 +30,9 @@ public void ParsesBlock() [Test] public void GeneratesAttributesInHtml() => - Html.ShouldContainHtml( - """

    this is regular text and this and this continues to be regular text

    """ + Html.Should().MatchInlineHtmlSnapshot( + """

    this is regular text and this and this continues to be regular text

    """, + DocsHtml.Body() ); } @@ -49,7 +53,10 @@ public void ParsesBlock() [Test] public void GeneratesAttributesInHtml() => // language=html - Html.Should().Be("""

    and this continues to be regular text

    """); + Html.Should().MatchInlineHtmlSnapshot( + """

    and this continues to be regular text

    """, + DocsHtml.Body() + ); } [InheritsTests] @@ -67,7 +74,7 @@ public void ParsesBlock() [Test] public void GeneratesAttributesInHtml() => // language=html - Html.ShouldContainHtml("""

    this is regular text and this

    """); + Html.Should().MatchInlineHtmlSnapshot("""

    this is regular text and this

    """, DocsHtml.Body()); } [InheritsTests] @@ -77,8 +84,7 @@ this is regular text and this $$is-an-inline-anchor$$$ { [Test] public void GeneratesAttributesInHtml() => - // language=html - Html.Should().Contain("""

    this is regular text and this $$is-an-inline-anchor$$$

    """); + Html.Should().MatchInlineHtmlSnapshot("

    this is regular text and this $$is-an-inline-anchor$$$

    ", DocsHtml.Body()); } [InheritsTests] @@ -89,7 +95,7 @@ this is regular text and this $$$is-an-inline-anchor$$ [Test] public void GeneratesAttributesInHtml() => // language=html - Html.ShouldContainHtml("""

    this is regular text and this $$$is-an-inline-anchor$$

    """); + Html.Should().MatchInlineHtmlSnapshot("""

    this is regular text and this $$$is-an-inline-anchor$$

    """, DocsHtml.Body()); } [InheritsTests] @@ -100,11 +106,12 @@ public class InlineAnchorInHeading() : BlockTest(""" [Test] public void GeneratesAttributesInHtml() => // language=html - Html.ShouldBeHtml( + Html.Should().MatchInlineHtmlSnapshot( """ - """ + """, + DocsHtml.Body() ); [Test] @@ -123,12 +130,13 @@ public class ExplicitSlugInHeader() : BlockTest(""" [Test] public void GeneratesAttributesInHtml() => // language=html - Html.ShouldBeHtml( + Html.Should().MatchInlineHtmlSnapshot( """ - """ + """, + DocsHtml.Body() ); } @@ -180,10 +188,26 @@ public class InlineAnchorCanBeLinkedToo() : InlineAnchorLinkTestBase(""" [Test] public void GeneratesHtml() => // language=html - Html.ShouldContainHtml("""

    Hello

    """); + Html.Should().MatchInlineHtmlSnapshot( + """ +
    +

    + Hello world +

    +
    +

    A paragraph

    +

    + Hello +

    +

    + +

    + """, + DocsHtml.Body() + ); [Test] - public void HasNoErrors() => Collector.Diagnostics.Should().HaveCount(0); + public void HasNoErrors() => Collector.Diagnostics.ToSnapshotText().Should().MatchInlineSnapshot(""); } [InheritsTests] @@ -193,12 +217,26 @@ [Sub Requirements](testing/req.md#custom-anchor) { [Test] public void GeneratesHtml() => - Html.ShouldContainHtml( - """

    Sub Requirements

    """ + Html.Should().MatchInlineHtmlSnapshot( + """ +
    +

    + Hello world +

    +
    +

    A paragraph

    +

    + Sub Requirements +

    +

    + +

    + """, + DocsHtml.Body() ); [Test] - public void HasNoErrors() => Collector.Diagnostics.Should().HaveCount(0); + public void HasNoErrors() => Collector.Diagnostics.ToSnapshotText().Should().MatchInlineSnapshot(""); } [InheritsTests] @@ -209,7 +247,7 @@ [Install steps](testing/req.md#install-steps) ) { [Test] - public void HasNoErrors() => Collector.Diagnostics.Should().HaveCount(0); + public void HasNoErrors() => Collector.Diagnostics.ToSnapshotText().Should().MatchInlineSnapshot(""); } [InheritsTests] @@ -222,7 +260,11 @@ [Install steps](testing/req.md#install-steps-install-anchor) // The heading renders as id="install-steps"; the inline anchor text is not part of its slug. [Test] public void HasAnError() => - Collector.Diagnostics.Should().ContainSingle().Which.Message.Should().Contain("install-steps-install-anchor"); + Collector + .Diagnostics + .ToSnapshotText() + .Should() + .MatchInlineSnapshot("error {repo}/docs/index.md:7 `install-steps-install-anchor` does not exist in testing/req.md."); } [InheritsTests] @@ -233,5 +275,5 @@ [Install steps](testing/req.md#install-anchor) ) { [Test] - public void HasNoErrors() => Collector.Diagnostics.Should().HaveCount(0); + public void HasNoErrors() => Collector.Diagnostics.ToSnapshotText().Should().MatchInlineSnapshot(""); } diff --git a/tests/Elastic.Markdown.Tests/Inline/InlineImageTest.cs b/tests/Elastic.Markdown.Tests/Inline/InlineImageTest.cs index 6bc36625e1..5d6b4f184e 100644 --- a/tests/Elastic.Markdown.Tests/Inline/InlineImageTest.cs +++ b/tests/Elastic.Markdown.Tests/Inline/InlineImageTest.cs @@ -2,7 +2,9 @@ // 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 Elastic.Documentation.Testing; using Markdig.Syntax.Inlines; +using Nullean.Snapshots.Html; namespace Elastic.Markdown.Tests.Inline; @@ -17,7 +19,10 @@ public class InlineImageTest() : InlineTest(""" [Test] public void GeneratesAttributesInHtml() => // language=html - Html.ShouldContainHtml("""

    Elasticsearch

    """); + Html.Should().MatchInlineHtmlSnapshot( + """

    Elasticsearch

    """, + DocsHtml.Body() + ); } [InheritsTests] @@ -31,7 +36,10 @@ public class RelativeInlineImageTest() : InlineTest(""" [Test] public void GeneratesAttributesInHtml() => // language=html - Html.ShouldContainHtml("""

    Elasticsearch

    """); + Html.Should().MatchInlineHtmlSnapshot( + """

    Elasticsearch

    """, + DocsHtml.Body() + ); } // Test image sizing with space before = @@ -48,8 +56,9 @@ public class InlineImageWithSizingSpaceBeforeTest() : InlineTest( [Test] public void GeneratesAttributesInHtml() => // language=html - Html.ShouldContainHtml( - """

    Elasticsearch

    """ + Html.Should().MatchInlineHtmlSnapshot( + """

    Elasticsearch

    """, + DocsHtml.Body() ); } @@ -67,8 +76,9 @@ public class InlineImageWithSizingNoSpaceBeforeTest() : InlineTest( [Test] public void GeneratesAttributesInHtml() => // language=html - Html.ShouldContainHtml( - """

    Elasticsearch

    """ + Html.Should().MatchInlineHtmlSnapshot( + """

    Elasticsearch

    """, + DocsHtml.Body() ); } @@ -84,8 +94,9 @@ public class InlineImageWithPixelSizingTest() : InlineTest(""" [Test] public void GeneratesAttributesInHtml() => // language=html - Html.ShouldContainHtml( - """

    Elasticsearch

    """ + Html.Should().MatchInlineHtmlSnapshot( + """

    Elasticsearch

    """, + DocsHtml.Body() ); } @@ -103,8 +114,9 @@ public class InlineImageWithTitleAndSizingTest() : InlineTest( [Test] public void GeneratesAttributesInHtml() => // language=html - Html.ShouldContainHtml( - """

    Elasticsearch

    """ + Html.Should().MatchInlineHtmlSnapshot( + """

    Elasticsearch

    """, + DocsHtml.Body() ); } @@ -120,7 +132,8 @@ public class InlineImageWithWidthOnlyTest() : InlineTest(""" [Test] public void GeneratesAttributesInHtml() => // language=html - Html.ShouldContainHtml( - """

    Elasticsearch

    """ + Html.Should().MatchInlineHtmlSnapshot( + """

    Elasticsearch

    """, + DocsHtml.Body() ); } diff --git a/tests/Elastic.Markdown.Tests/Inline/InlineLinkTests.cs b/tests/Elastic.Markdown.Tests/Inline/InlineLinkTests.cs index 425e28b63b..53e333e39f 100644 --- a/tests/Elastic.Markdown.Tests/Inline/InlineLinkTests.cs +++ b/tests/Elastic.Markdown.Tests/Inline/InlineLinkTests.cs @@ -5,8 +5,11 @@ using System.IO.Abstractions.TestingHelpers; using AwesomeAssertions; using Elastic.Documentation.Diagnostics; +using Elastic.Documentation.Testing; using JetBrains.Annotations; using Markdig.Syntax.Inlines; +using Nullean.Snapshots; +using Nullean.Snapshots.Html; namespace Elastic.Markdown.Tests.Inline; @@ -43,12 +46,17 @@ public class InlineLinkTests() : LinkTestBase(""" { [Test] public void GeneratesHtml() => - Html.ShouldContainHtml( - """

    Elasticsearch

    """ + Html.Should().MatchInlineHtmlSnapshot( + """ +

    + Elasticsearch +

    + """, + DocsHtml.Body() ); [Test] - public void HasNoErrors() => Collector.Diagnostics.Should().HaveCount(0); + public void HasNoErrors() => Collector.Diagnostics.ToSnapshotText().Should().MatchInlineSnapshot(""); } [InheritsTests] @@ -58,12 +66,17 @@ public class LinkToPageTests() : LinkTestBase(""" { [Test] public void GeneratesHtml() => - Html.ShouldContainHtml( - """

    Requirements

    """ + Html.Should().MatchInlineHtmlSnapshot( + """ +

    + Requirements +

    + """, + DocsHtml.Body() ); [Test] - public void HasNoErrors() => Collector.Diagnostics.Should().HaveCount(0); + public void HasNoErrors() => Collector.Diagnostics.ToSnapshotText().Should().MatchInlineSnapshot(""); [Test] public void EmitsCrossLink() => Collector.CrossLinks.Should().HaveCount(0); @@ -76,12 +89,17 @@ public class InsertPageTitleTests() : LinkTestBase(""" { [Test] public void GeneratesHtml() => - Html.ShouldContainHtml( - """

    Special Requirements

    """ + Html.Should().MatchInlineHtmlSnapshot( + """ +

    + Special Requirements +

    + """, + DocsHtml.Body() ); [Test] - public void HasNoErrors() => Collector.Diagnostics.Should().HaveCount(0); + public void HasNoErrors() => Collector.Diagnostics.ToSnapshotText().Should().MatchInlineSnapshot(""); [Test] public void EmitsCrossLink() => Collector.CrossLinks.Should().HaveCount(0); @@ -96,12 +114,17 @@ public class RepositoryLinksTest() : LinkTestBase(""" { [Test] public void GeneratesHtml() => - Html.ShouldContainHtml( - """

    test

    """ + Html.Should().MatchInlineHtmlSnapshot( + """ +

    + test +

    + """, + DocsHtml.Body() ); [Test] - public void HasNoErrors() => Collector.Diagnostics.Should().HaveCount(0); + public void HasNoErrors() => Collector.Diagnostics.ToSnapshotText().Should().MatchInlineSnapshot(""); [Test] public void EmitsCrossLink() => Collector.CrossLinks.Should().HaveCount(0); @@ -116,12 +139,13 @@ public class CrossLinkReferenceTest() : LinkTestBase(""" { [Test] public void GeneratesHtml() => - Html.ShouldContainHtml( - """

    test

    """ + Html.Should().MatchInlineHtmlSnapshot( + """

    test

    """, + DocsHtml.Body() ); [Test] - public void HasNoErrors() => Collector.Diagnostics.Should().HaveCount(0); + public void HasNoErrors() => Collector.Diagnostics.ToSnapshotText().Should().MatchInlineSnapshot(""); [Test] public void EmitsCrossLink() @@ -139,13 +163,17 @@ Go to [test](kibana://index.md) { [Test] public void GeneratesHtml() => - // language=html - Html.Should().Contain( - """

    Go to test

    """ + Html.Should().MatchInlineHtmlSnapshot( + """ +

    Go to + test +

    + """, + DocsHtml.Body() ); [Test] - public void HasNoErrors() => Collector.Diagnostics.Should().HaveCount(0); + public void HasNoErrors() => Collector.Diagnostics.ToSnapshotText().Should().MatchInlineSnapshot(""); [Test] public void EmitsCrossLink() @@ -163,14 +191,24 @@ Go to [](kibana://index.md) { [Test] public void GeneratesHtml() => - // language=html - empty crosslinks now emit an error - Html.Should().Contain( - """

    Go to

    """ + Html.Should().MatchInlineHtmlSnapshot( + """ +

    Go to + +

    + """, + DocsHtml.Body() ); [Test] public void HasError() => - Collector.Diagnostics.Should().Contain(d => d.Severity == Severity.Error && d.Message.Contains("empty link text")); + Collector + .Diagnostics + .ToSnapshotText() + .Should() + .MatchInlineSnapshot( + "error {repo}/docs/index.md:4 Crosslink 'kibana://index.md' has empty link text. Please provide explicit link text." + ); [Test] public void EmitsCrossLink() @@ -188,14 +226,24 @@ Go to [](kibana://get-started/index.md) { [Test] public void GeneratesHtml() => - // language=html - empty crosslinks emit an error; isolated builds get target=_blank - Html.Should().Contain( - """

    Go to

    """ + Html.Should().MatchInlineHtmlSnapshot( + """ +

    Go to + +

    + """, + DocsHtml.Body() ); [Test] public void HasError() => - Collector.Diagnostics.Should().Contain(d => d.Severity == Severity.Error && d.Message.Contains("empty link text")); + Collector + .Diagnostics + .ToSnapshotText() + .Should() + .MatchInlineSnapshot( + "error {repo}/docs/index.md:4 Crosslink 'kibana://get-started/index.md' has empty link text. Please provide explicit link text." + ); [Test] public void EmitsCrossLink() @@ -213,19 +261,14 @@ [global search field]({{this-variable-does-not-exist}}/introduction.html#kibana- ) { [Test] - public void HasErrors() - { - Collector.Diagnostics.Should().HaveCount(1); - Collector.Diagnostics.First().Severity.Should().Be(Severity.Error); + public void HasErrors() => Collector .Diagnostics - .First() - .Message + .ToSnapshotText() .Should() - .Contain( - "he url contains unresolved template expressions: '{{this-variable-does-not-exist}}/introduction.html#kibana-navigation-search'. Please check if there is an appropriate global or frontmatter subs variable." + .MatchInlineSnapshot( + "error {repo}/docs/index.md:3 The url contains unresolved template expressions: '{{this-variable-does-not-exist}}/introduction.html#kibana-navigation-search'. Please check if there is an appropriate global or frontmatter subs variable." ); - } } [InheritsTests] @@ -235,12 +278,13 @@ [link to app]({{some-url-with-a-version}}) { [Test] public void GeneratesHtml() => - Html.ShouldContainHtml( - """

    link to app

    """ + Html.Should().MatchInlineHtmlSnapshot( + """

    link to app

    """, + DocsHtml.Body() ); [Test] - public void HasNoWarningsOrErrors() => Collector.Diagnostics.Should().HaveCount(0); + public void HasNoWarningsOrErrors() => Collector.Diagnostics.ToSnapshotText().Should().MatchInlineSnapshot(""); } [InheritsTests] @@ -249,19 +293,14 @@ [link to app]({{some-url-path-prefix}}/hello-world) """) { [Test] - public void HasWarnings() - { - Collector.Diagnostics.Should().HaveCount(1); - Collector.Diagnostics.First().Severity.Should().Be(Severity.Error); + public void HasWarnings() => Collector .Diagnostics - .First() - .Message + .ToSnapshotText() .Should() - .Contain( - "Link is resolved to '/something/hello-world'. Only external links are allowed to be resolved from template expressions." + .MatchInlineSnapshot( + "error {repo}/docs/index.md:3 Link is resolved to '/something/hello-world'. Only external links are allowed to be resolved from template expressions." ); - } } [InheritsTests] @@ -270,10 +309,14 @@ [Non Existing Link](/non-existing.md) """) { [Test] - public void HasErrors() => Collector.Diagnostics.Where(d => d.Severity == Severity.Error).Should().HaveCount(1); - - [Test] - public void HasNoWarning() => Collector.Diagnostics.Where(d => d.Severity == Severity.Warning).Should().HaveCount(0); + public void EmitsDiagnostics() => + Collector + .Diagnostics + .ToSnapshotText() + .Should() + .MatchInlineSnapshot( + "error {repo}/docs/index.md:3 `/non-existing.md` does not exist. If it was recently removed add a redirect. resolved to `{repo}/docs/non-existing.md`" + ); } [InheritsTests] @@ -287,7 +330,7 @@ public void GeneratesHtml() => Html.Should().BeNullOrWhiteSpace(); [Test] - public void HasErrors() => Collector.Diagnostics.Should().HaveCount(0); + public void HasErrors() => Collector.Diagnostics.ToSnapshotText().Should().MatchInlineSnapshot(""); } [InheritsTests] @@ -303,7 +346,7 @@ public class CommentedNonExistingLinks2() : LinkTestBase( { [Test] public void GeneratesHtml() => - Html.ShouldBeHtml( + Html.Should().MatchInlineHtmlSnapshot( """

    Links:

      @@ -312,11 +355,12 @@ public void GeneratesHtml() => - """ + """, + DocsHtml.Body() ); [Test] - public void HasErrors() => Collector.Diagnostics.Should().HaveCount(0); + public void HasErrors() => Collector.Diagnostics.ToSnapshotText().Should().MatchInlineSnapshot(""); } [InheritsTests] @@ -330,7 +374,18 @@ This is another [Non Existing Link](/non-existing.md) ) { [Test] - public void HasErrors() => Collector.Diagnostics.Should().HaveCount(3); + public void HasErrors() => + Collector + .Diagnostics + .ToSnapshotText() + .Should() + .MatchInlineSnapshot( + """ + error {repo}/docs/index.md:3 `/non-existing.md` does not exist. If it was recently removed add a redirect. resolved to `{repo}/docs/non-existing.md` + error {repo}/docs/index.md:4 `/non-existing.md` does not exist. If it was recently removed add a redirect. resolved to `{repo}/docs/non-existing.md` + error {repo}/docs/index.md:5 `/non-existing.md` does not exist. If it was recently removed add a redirect. resolved to `{repo}/docs/non-existing.md` + """ + ); } [InheritsTests] @@ -341,10 +396,18 @@ [Install with Cursor](cursor://anysphere.cursor-deeplink/mcp/install?name=elasti ) { [Test] - public void GeneratesHtml() => Html.Should().Contain("""href="cursor://"""); + public void GeneratesHtml() => + Html.Should().MatchInlineHtmlSnapshot( + """ +

      + Install with Cursor +

      + """, + DocsHtml.Body() + ); [Test] - public void HasNoErrors() => Collector.Diagnostics.Should().HaveCount(0); + public void HasNoErrors() => Collector.Diagnostics.ToSnapshotText().Should().MatchInlineSnapshot(""); [Test] public void EmitsNoCrossLinks() => Collector.CrossLinks.Should().HaveCount(0); @@ -356,10 +419,18 @@ [Install VS Code Extension](vscode:extension/elastic.elasticsearch) """) { [Test] - public void GeneratesHtml() => Html.Should().Contain("""href="vscode:"""); + public void GeneratesHtml() => + Html.Should().MatchInlineHtmlSnapshot( + """ +

      + Install VS Code Extension +

      + """, + DocsHtml.Body() + ); [Test] - public void HasNoErrors() => Collector.Diagnostics.Should().HaveCount(0); + public void HasNoErrors() => Collector.Diagnostics.ToSnapshotText().Should().MatchInlineSnapshot(""); [Test] public void EmitsNoCrossLinks() => Collector.CrossLinks.Should().HaveCount(0); @@ -373,10 +444,18 @@ [Install with VS Code Insiders](vscode-insiders:mcp/install?%7B%22name%22%3A%22o ) { [Test] - public void GeneratesHtml() => Html.Should().Contain("""href="vscode-insiders:"""); + public void GeneratesHtml() => + Html.Should().MatchInlineHtmlSnapshot( + """ +

      + Install with VS Code Insiders +

      + """, + DocsHtml.Body() + ); [Test] - public void HasNoErrors() => Collector.Diagnostics.Should().HaveCount(0); + public void HasNoErrors() => Collector.Diagnostics.ToSnapshotText().Should().MatchInlineSnapshot(""); [Test] public void EmitsNoCrossLinks() => Collector.CrossLinks.Should().HaveCount(0); @@ -413,7 +492,7 @@ protected override void AddToFileSystem(MockFileSystem fileSystem) } [Test] - public void HasNoErrors() => Collector.Diagnostics.Should().HaveCount(0); + public void HasNoErrors() => Collector.Diagnostics.ToSnapshotText().Should().MatchInlineSnapshot(""); } [InheritsTests] diff --git a/tests/Elastic.Markdown.Tests/Inline/MathRoleTests.cs b/tests/Elastic.Markdown.Tests/Inline/MathRoleTests.cs index 960a18cb80..344c4f8f37 100644 --- a/tests/Elastic.Markdown.Tests/Inline/MathRoleTests.cs +++ b/tests/Elastic.Markdown.Tests/Inline/MathRoleTests.cs @@ -3,6 +3,8 @@ // See the LICENSE file in the project root for more information using AwesomeAssertions; +using Elastic.Documentation.Testing; +using Nullean.Snapshots.Html; namespace Elastic.Markdown.Tests.Inline; @@ -11,7 +13,15 @@ public class MathRoleTests() : InlineTest(""" """) { [Test] - public void Render() => Html.Should().Contain("E = mc^2").And.NotContain("{math}`E = mc^2`"); + public void Render() => + Html.Should().MatchInlineHtmlSnapshot( + """ +

      Einstein's famous equation + E = mc^2 + relates energy and mass.

      + """, + DocsHtml.Body() + ); } public class MathRoleEscapesContentTests() : InlineTest(""" @@ -19,5 +29,13 @@ public class MathRoleEscapesContentTests() : InlineTest(""" """) { [Test] - public void Render() => Html.Should().Contain("a < b & b > c"); + public void Render() => + Html.Should().MatchInlineHtmlSnapshot( + """ +

      Compare + a < b & b > c + for ordering.

      + """, + DocsHtml.Body() + ); } diff --git a/tests/Elastic.Markdown.Tests/Inline/SubstitutionInlineCodeTest.cs b/tests/Elastic.Markdown.Tests/Inline/SubstitutionInlineCodeTest.cs index 4631aac3bc..3df00c0e60 100644 --- a/tests/Elastic.Markdown.Tests/Inline/SubstitutionInlineCodeTest.cs +++ b/tests/Elastic.Markdown.Tests/Inline/SubstitutionInlineCodeTest.cs @@ -3,6 +3,9 @@ // See the LICENSE file in the project root for more information using AwesomeAssertions; +using Elastic.Documentation.Testing; +using Nullean.Snapshots; +using Nullean.Snapshots.Html; namespace Elastic.Markdown.Tests.Inline; @@ -27,17 +30,27 @@ public class SubstitutionInlineCodeTest() : InlineTest( ) { [Test] - public void TestSubstitutionInlineCode() - { - // Check that regular code blocks are not processed - Html.Should().Contain("wget elasticsearch-{{version}}.tar.gz"); - - // Check that {subs} inline code blocks have substitutions applied - Html.Should().Contain("wget elasticsearch-8.15.0.tar.gz"); - Html.Should().Contain("export MY_VAR=8.15.0"); - Html.Should().Contain("version 8.15"); - } + // Check that regular code blocks are not processed + // Check that {subs} inline code blocks have substitutions applied + public void TestSubstitutionInlineCode() => + Html.Should().MatchInlineHtmlSnapshot( + """ +

      Regular code: + wget elasticsearch-{{version}}.tar.gz +

      +

      Code with substitutions: + wget elasticsearch-8.15.0.tar.gz +

      +

      Multiple substitutions: + export MY_VAR=8.15.0 +

      +

      With mutations: + version 8.15 +

      + """, + DocsHtml.Body() + ); [Test] - public void HasNoErrors() => Collector.Diagnostics.Should().HaveCount(0); + public void HasNoErrors() => Collector.Diagnostics.ToSnapshotText().Should().MatchInlineSnapshot(""); } diff --git a/tests/Elastic.Markdown.Tests/Inline/SubstitutionTest.cs b/tests/Elastic.Markdown.Tests/Inline/SubstitutionTest.cs index 67e87856dd..1e569de9fb 100644 --- a/tests/Elastic.Markdown.Tests/Inline/SubstitutionTest.cs +++ b/tests/Elastic.Markdown.Tests/Inline/SubstitutionTest.cs @@ -3,8 +3,11 @@ // See the LICENSE file in the project root for more information using AwesomeAssertions; +using Elastic.Documentation.Testing; using Elastic.Markdown.Myst.CodeBlocks; using Elastic.Markdown.Myst.InlineParsers.Substitution; +using Nullean.Snapshots; +using Nullean.Snapshots.Html; namespace Elastic.Markdown.Tests.Inline; @@ -22,7 +25,7 @@ not a comment { [Test] public void ReplacesSubsFromFrontMatter() => - Html.Should().Contain("""Hello World!""").And.Contain("""not a comment""").And.NotContain("""{{hello-world}}"""); + Html.Should().MatchInlineHtmlSnapshot("

      The following should be subbed: Hello World! not a comment

      ", DocsHtml.Body()); } public class NeedsDoubleBrackets() : InlineTest( @@ -42,19 +45,12 @@ not a {substitution} ) { [Test] + // treated as attributes to the block public void PreservesSingleBracket() => - Html - .Should() - .Contain("""Hello World!""") - .And - .Contain("""not a comment""") - .And - .NotContain("""{{hello-world}}""") - .And - .Contain( // treated as attributes to the block - """{substitution}""") - .And - .Contain("""{{valid-key}}"""); + Html.Should().MatchInlineHtmlSnapshot( + "

      The following should be subbed: Hello World! not a comment not a {{valid-key}} not a {substitution}

      ", + DocsHtml.Body() + ); } [InheritsTests] @@ -78,7 +74,23 @@ cd elasticsearch-{{version}}/ <2> ) { [Test] - public void ReplacesSubsInCode() => Html.Should().Contain("7.17.0"); + public void ReplacesSubsInCode() => + Html.Should().MatchInlineHtmlSnapshot( + """ +
      +
      +
      +					wget https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-7.17.0-linux-x86_64.tar.gz wget https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-7.17.0-linux-x86_64.tar.gz.sha512 shasum -a 512 -c elasticsearch-7.17.0-linux-x86_64.tar.gz.sha512
      +						
      +						tar -xzf elasticsearch-7.17.0-linux-x86_64.tar.gz cd elasticsearch-7.17.0/
      +						
      +					
      +				
      +
      +
      + """, + DocsHtml.Body() + ); } public class SupportsSubstitutionsFromDocSet() : InlineTest( @@ -95,15 +107,10 @@ public class SupportsSubstitutionsFromDocSet() : InlineTest( { [Test] public void EmitsGlobalVariable() => - Html - .Should() - .Contain("Hello World!") - .And - .NotContain("{{hello-world}}") - .And - .Contain("A variable from docset.yml") - .And - .NotContain("{{global-var}}"); + Html.Should().MatchInlineHtmlSnapshot( + "

      The following should be subbed: Hello World! The following should be subbed as well: A variable from docset.yml

      ", + DocsHtml.Body() + ); } public class CanNotShadeGlobalVariables() : InlineTest( @@ -123,16 +130,20 @@ public class CanNotShadeGlobalVariables() : InlineTest( { [Test] public void OnlySeesGlobalVariable() => - Html.Should().NotContain("Hello World!
      ").And.NotContain("{{hello-world}}").And.Contain("A variable from docset.yml"); + Html.Should().MatchInlineHtmlSnapshot( + "

      The following should be subbed: A variable from docset.yml The following should be subbed as well: A variable from docset.yml

      ", + DocsHtml.Body() + ); [Test] public void HasError() => Collector .Diagnostics + .ToSnapshotText() .Should() - .HaveCount(1) - .And - .Contain(d => d.Message.Contains("{hello-world} can not be redeclared in front matter as its a global substitution")); + .MatchInlineSnapshot( + "error {repo}/docs/index.md {hello-world} can not be redeclared in front matter as its a global substitution" + ); } public class ReplaceInHeader() : InlineTest( @@ -151,10 +162,19 @@ public class ReplaceInHeader() : InlineTest( { [Test] public void OnlySeesGlobalVariable() => - Html.ShouldContainHtml("""

      Hello World!

      """); + Html.Should().MatchInlineHtmlSnapshot( + """ +
      +

      + Hello World! +

      +
      + """, + DocsHtml.Body() + ); [Test] - public void HasNoErrors() => Collector.Diagnostics.Should().HaveCount(0); + public void HasNoErrors() => Collector.Diagnostics.ToSnapshotText().Should().MatchInlineSnapshot(""); } public class ReplaceInImageAlt() : InlineTest( @@ -171,7 +191,15 @@ public class ReplaceInImageAlt() : InlineTest( ) { [Test] - public void OnlySeesGlobalVariable() => Html.Should().NotContain("alt=\"{{hello-world}}\"").And.Contain("alt=\"Hello World\""); + public void OnlySeesGlobalVariable() => + Html.Should().MatchInlineHtmlSnapshot( + """ +

      + Hello World +

      + """, + DocsHtml.Body() + ); } public class ReplaceInImageTitle() : InlineTest( @@ -188,7 +216,15 @@ public class ReplaceInImageTitle() : InlineTest( ) { [Test] - public void OnlySeesGlobalVariable() => Html.Should().NotContain("title=\"{{hello-world}}\"").And.Contain("title=\"Observability\""); + public void OnlySeesGlobalVariable() => + Html.Should().MatchInlineHtmlSnapshot( + """ +

      + Observability +

      + """, + DocsHtml.Body() + ); } public class MutationOperatorTest() : InlineTest( @@ -214,34 +250,15 @@ public class MutationOperatorTest() : InlineTest( ) { [Test] - public void MutationOperatorsWorkWithAndWithoutSpaces() - { - // Both versions with and without spaces should render the same way - Html - .Should() - .Contain("Version: 9.0") - .And - .Contain("Version with space: 9.0") - .And - .Contain("Major only: 9") - .And - .Contain("Major only with space: 9") - .And - .Contain("Major.x: 9.x") - .And - .Contain("Major.x with space: 9.x") - .And - .Contain("Increase major: 10.0.0") - .And - .Contain("Increase major with space: 10.0.0") - .And - .Contain("Increase minor: 9.1.0") - .And - .Contain("Increase minor with space: 9.1.0"); - } + // Both versions with and without spaces should render the same way + public void MutationOperatorsWorkWithAndWithoutSpaces() => + Html.Should().MatchInlineHtmlSnapshot( + "

      Version: 9.0 Version with space: 9.0 Major only: 9 Major only with space: 9 Major.x: 9.x Major.x with space: 9.x Increase major: 10.0.0 Increase major with space: 10.0.0 Increase minor: 9.1.0 Increase minor with space: 9.1.0

      ", + DocsHtml.Body() + ); [Test] - public void HasNoErrors() => Collector.Diagnostics.Should().HaveCount(0); + public void HasNoErrors() => Collector.Diagnostics.ToSnapshotText().Should().MatchInlineSnapshot(""); } public class MultipleMutationOperatorsTest() : InlineTest( @@ -262,22 +279,15 @@ public class MultipleMutationOperatorsTest() : InlineTest( ) { [Test] - public void MultipleMutationOperatorsWorkWithAndWithoutSpaces() - { - // Both versions with and without spaces should render the same way - Html - .Should() - .Contain("Version: 9.0") - .And - .Contain("Version with spaces: 9.0") - .And - .Contain("Product: ELASTICSEARCH") - .And - .Contain("Product with spaces: ELASTICSEARCH"); - } + // Both versions with and without spaces should render the same way + public void MultipleMutationOperatorsWorkWithAndWithoutSpaces() => + Html.Should().MatchInlineHtmlSnapshot( + "

      Version: 9.0 Version with spaces: 9.0 Product: ELASTICSEARCH Product with spaces: ELASTICSEARCH

      ", + DocsHtml.Body() + ); [Test] - public void HasNoErrors() => Collector.Diagnostics.Should().HaveCount(0); + public void HasNoErrors() => Collector.Diagnostics.ToSnapshotText().Should().MatchInlineSnapshot(""); } public class MutationOperatorsInLinksTest() : InlineTest( @@ -299,26 +309,22 @@ [Link text with mutation and space]({{product | uc}} {{version | M.M}}) ) { [Test] - public void MutationOperatorsWorkInLinks() - { - // Check URL mutations - Html - .Should() - .Contain("href=\"https://www.elastic.co/guide/en/elasticsearch/reference/9.0/index.html\"") - .And - .NotContain("{{version|M.M}}") - .And - .NotContain("{{version | M.M}}"); - - // Check link text mutations - Html.Should().Contain("ELASTICSEARCH 9.0").And.NotContain("{{product|uc}}").And.NotContain("{{version|M.M}}"); - - // Check link text mutations with spaces - Html.Should().Contain("ELASTICSEARCH 9.0").And.NotContain("{{product | uc}}").And.NotContain("{{version | M.M}}"); - } + // Check URL mutations + // Check link text mutations + // Check link text mutations with spaces + public void MutationOperatorsWorkInLinks() => + Html.Should().MatchInlineHtmlSnapshot( + """ +

      + Link with mutation operator + Link with mutation operator and space + [Link text with mutation](ELASTICSEARCH 9.0) [Link text with mutation and space](ELASTICSEARCH 9.0)

      + """, + DocsHtml.Body() + ); [Test] - public void HasNoErrors() => Collector.Diagnostics.Should().HaveCount(0); + public void HasNoErrors() => Collector.Diagnostics.ToSnapshotText().Should().MatchInlineSnapshot(""); } [InheritsTests] @@ -344,16 +350,20 @@ public class MutationOperatorsInCodeBlocksTest() : BlockTest( { [Test] public void MutationOperatorsWorkInCodeBlocks() => - Html - .Should() - .Contain("# Install Elasticsearch 9.0") - .And - .Contain("elasticsearch-9.0-linux-x86_64.tar.gz") - .And - .NotContain("{{version|M.M}}") - .And - .NotContain("{{version | M.M}}"); + Html.Should().MatchInlineHtmlSnapshot( + """ +
      +
      +
      +					# Install Elasticsearch 9.0 wget https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-9.0-linux-x86_64.tar.gz  # With space in mutation wget https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-9.0-linux-x86_64.tar.gz
      +					
      +				
      +
      +
      + """, + DocsHtml.Body() + ); [Test] - public void HasNoErrors() => Collector.Diagnostics.Should().HaveCount(0); + public void HasNoErrors() => Collector.Diagnostics.ToSnapshotText().Should().MatchInlineSnapshot(""); } diff --git a/tests/Elastic.Markdown.Tests/MissingTocFileTests.cs b/tests/Elastic.Markdown.Tests/MissingTocFileTests.cs index d3393b0b2d..aadd5f114c 100644 --- a/tests/Elastic.Markdown.Tests/MissingTocFileTests.cs +++ b/tests/Elastic.Markdown.Tests/MissingTocFileTests.cs @@ -6,7 +6,9 @@ using AwesomeAssertions; using Elastic.Documentation.Configuration; using Elastic.Documentation.Diagnostics; +using Elastic.Documentation.Testing; using Elastic.Markdown.IO; +using Nullean.Snapshots; namespace Elastic.Markdown.Tests; @@ -47,10 +49,15 @@ public void TocReferencesMissingFile_DoesNotThrow_AndEmitsClearError() act.Should().NotThrow("a missing toc file must surface a validation error, not crash the build"); - collector.Errors.Should().BeGreaterThan(0); collector .Diagnostics + .ToSnapshotText() .Should() - .Contain(d => d.Severity == Severity.Error && d.Message.Contains("missing.md") && d.Message.Contains("does not exist")); + .MatchInlineSnapshot( + """ + error {repo}/docs/docset.yml Table of contents references 'missing.md' but the file does not exist on disk. + error {repo}/docs/docset.yml Documentation set 'test' ({repo}/docs/docset.yml) table of contents has items defined but none could be created + """ + ); } } diff --git a/tests/Elastic.Markdown.Tests/RootIndexValidationTests.cs b/tests/Elastic.Markdown.Tests/RootIndexValidationTests.cs index 5470eac332..3e401cf490 100644 --- a/tests/Elastic.Markdown.Tests/RootIndexValidationTests.cs +++ b/tests/Elastic.Markdown.Tests/RootIndexValidationTests.cs @@ -7,7 +7,9 @@ using AwesomeAssertions; using Elastic.Documentation.Configuration; using Elastic.Documentation.Diagnostics; +using Elastic.Documentation.Testing; using Elastic.Markdown.IO; +using Nullean.Snapshots; namespace Elastic.Markdown.Tests; @@ -41,8 +43,11 @@ public void InternalRegistry_MissingIndexMd_EmitsError() var context = new BuildContext(collector, TestHelpers.CreateDocumentationFileSystem(fileSystem), configurationContext); _ = new DocumentationSet(context, logger, new TestCrossLinkResolver()); - collector.Errors.Should().BeGreaterThan(0); - collector.Diagnostics.Should().Contain(d => d.Severity == Severity.Error && d.Message.Contains("index.md")); + collector + .Diagnostics + .ToSnapshotText() + .Should() + .MatchInlineSnapshot("error {repo}/docs/docset.yml Non-public documentation sets require a root index.md file"); } [Test] diff --git a/tests/Elastic.Markdown.Tests/Search/DocumentationDocumentSerializationTests.cs b/tests/Elastic.Markdown.Tests/Search/DocumentationDocumentSerializationTests.cs index d39a0e8e66..7d13103c2f 100644 --- a/tests/Elastic.Markdown.Tests/Search/DocumentationDocumentSerializationTests.cs +++ b/tests/Elastic.Markdown.Tests/Search/DocumentationDocumentSerializationTests.cs @@ -10,6 +10,7 @@ using Elastic.Documentation.Search; using Elastic.Documentation.Search.Contract; using Elastic.Documentation.Versions; +using Nullean.Snapshots.Json; namespace Elastic.Markdown.Tests.Search; @@ -30,22 +31,37 @@ public void SerializeDocumentWithStackAppliesToProducesCorrectJson() }; var json = JsonSerializer.Serialize(doc, _options); - var jsonDoc = JsonDocument.Parse(json); - var root = jsonDoc.RootElement; - - // Verify applies_to existing - root.TryGetProperty("applies_to", out var appliesTo).Should().BeTrue(); - appliesTo.ValueKind.Should().Be(JsonValueKind.Array); - - // Verify structure - var appliesArray = appliesTo.EnumerateArray().ToList(); - appliesArray.Should().HaveCount(1); - - var stackEntry = appliesArray[0]; - stackEntry.GetProperty("type").GetString().Should().Be("stack"); - stackEntry.GetProperty("sub_type").GetString().Should().Be("stack"); - stackEntry.GetProperty("lifecycle").GetString().Should().Be("ga"); - stackEntry.GetProperty("version").GetString().Should().Be("all"); + json.Should().MatchInlineJsonSnapshot( + """ + { + "applies_to": [ + { + "type": "stack", + "sub_type": "stack", + "lifecycle": "ga", + "version": "all" + } + ], + "content_hash": "", + "content_last_updated": "0001-01-01T00:00:00+00:00", + "title": "Test Page", + "search_title": "Test Page", + "content_type": "doc", + "path": "/test/page", + "url": "/test/page", + "hash": "", + "batch_index_date": "0001-01-01T00:00:00+00:00", + "last_updated": "0001-01-01T00:00:00+00:00", + "headings": [], + "parents": [], + "content_tier": "reference", + "navigation": { + "depth": 50, + "table_of_contents": 50 + } + } + """ + ); } [Test] @@ -68,26 +84,43 @@ public void SerializeDocumentWithDeploymentAppliesToProducesCorrectJson() }; var json = JsonSerializer.Serialize(doc, _options); - var jsonDoc = JsonDocument.Parse(json); - var root = jsonDoc.RootElement; - - root.TryGetProperty("applies_to", out var appliesTo).Should().BeTrue(); - var appliesArray = appliesTo.EnumerateArray().ToList(); - appliesArray.Should().HaveCount(2); - - // Verify ESS entry - var essEntry = appliesArray.FirstOrDefault(e => e.GetProperty("sub_type").GetString() == "ess"); - essEntry.ValueKind.Should().NotBe(JsonValueKind.Undefined); - essEntry.GetProperty("type").GetString().Should().Be("deployment"); - essEntry.GetProperty("lifecycle").GetString().Should().Be("ga"); - essEntry.GetProperty("version").GetString().Should().Be("all"); - - // Verify ECE entry - var eceEntry = appliesArray.FirstOrDefault(e => e.GetProperty("sub_type").GetString() == "ece"); - eceEntry.ValueKind.Should().NotBe(JsonValueKind.Undefined); - eceEntry.GetProperty("type").GetString().Should().Be("deployment"); - eceEntry.GetProperty("lifecycle").GetString().Should().Be("beta"); - eceEntry.GetProperty("version").GetString().Should().Be("3.5+"); + json.Should().MatchInlineJsonSnapshot( + """ + { + "applies_to": [ + { + "type": "deployment", + "sub_type": "ece", + "lifecycle": "beta", + "version": "3.5+" + }, + { + "type": "deployment", + "sub_type": "ess", + "lifecycle": "ga", + "version": "all" + } + ], + "content_hash": "", + "content_last_updated": "0001-01-01T00:00:00+00:00", + "title": "Deployment Test", + "search_title": "Deployment Test", + "content_type": "doc", + "path": "/test/deployment", + "url": "/test/deployment", + "hash": "", + "batch_index_date": "0001-01-01T00:00:00+00:00", + "last_updated": "0001-01-01T00:00:00+00:00", + "headings": [], + "parents": [], + "content_tier": "reference", + "navigation": { + "depth": 50, + "table_of_contents": 50 + } + } + """ + ); } [Test] @@ -115,33 +148,49 @@ public void SerializeDocumentWithServerlessAppliesToProducesCorrectJson() }; var json = JsonSerializer.Serialize(doc, _options); - var jsonDoc = JsonDocument.Parse(json); - var root = jsonDoc.RootElement; - - root.TryGetProperty("applies_to", out var appliesTo).Should().BeTrue(); - var appliesArray = appliesTo.EnumerateArray().ToList(); - appliesArray.Should().HaveCount(3); - - // Verify elasticsearch entry - var esEntry = appliesArray.FirstOrDefault(e => e.GetProperty("sub_type").GetString() == "elasticsearch"); - esEntry.ValueKind.Should().NotBe(JsonValueKind.Undefined); - esEntry.GetProperty("type").GetString().Should().Be("serverless"); - esEntry.GetProperty("lifecycle").GetString().Should().Be("ga"); - esEntry.GetProperty("version").GetString().Should().Be("8.0+"); - - // Verify security entry - var secEntry = appliesArray.FirstOrDefault(e => e.GetProperty("sub_type").GetString() == "security"); - secEntry.ValueKind.Should().NotBe(JsonValueKind.Undefined); - secEntry.GetProperty("type").GetString().Should().Be("serverless"); - secEntry.GetProperty("lifecycle").GetString().Should().Be("preview"); - secEntry.GetProperty("version").GetString().Should().Be("1.0+"); - - // Verify Vector Database entry - var vectorDatabaseEntry = appliesArray.FirstOrDefault(e => e.GetProperty("sub_type").GetString() == "vectordb"); - vectorDatabaseEntry.ValueKind.Should().NotBe(JsonValueKind.Undefined); - vectorDatabaseEntry.GetProperty("type").GetString().Should().Be("serverless"); - vectorDatabaseEntry.GetProperty("lifecycle").GetString().Should().Be("ga"); - vectorDatabaseEntry.GetProperty("version").GetString().Should().Be("all"); + json.Should().MatchInlineJsonSnapshot( + """ + { + "applies_to": [ + { + "type": "serverless", + "sub_type": "elasticsearch", + "lifecycle": "ga", + "version": "8.0+" + }, + { + "type": "serverless", + "sub_type": "security", + "lifecycle": "preview", + "version": "1.0+" + }, + { + "type": "serverless", + "sub_type": "vectordb", + "lifecycle": "ga", + "version": "all" + } + ], + "content_hash": "", + "content_last_updated": "0001-01-01T00:00:00+00:00", + "title": "Serverless Test", + "search_title": "Serverless Test", + "content_type": "doc", + "path": "/test/serverless", + "url": "/test/serverless", + "hash": "", + "batch_index_date": "0001-01-01T00:00:00+00:00", + "last_updated": "0001-01-01T00:00:00+00:00", + "headings": [], + "parents": [], + "content_tier": "reference", + "navigation": { + "depth": 50, + "table_of_contents": 50 + } + } + """ + ); } [Test] @@ -160,18 +209,37 @@ public void SerializeDocumentWithProductAppliesToProducesCorrectJson() }; var json = JsonSerializer.Serialize(doc, _options); - var jsonDoc = JsonDocument.Parse(json); - var root = jsonDoc.RootElement; - - root.TryGetProperty("applies_to", out var appliesTo).Should().BeTrue(); - var appliesArray = appliesTo.EnumerateArray().ToList(); - appliesArray.Should().HaveCount(1); - - var productEntry = appliesArray[0]; - productEntry.GetProperty("type").GetString().Should().Be("product"); - productEntry.GetProperty("sub_type").GetString().Should().Be("product"); - productEntry.GetProperty("lifecycle").GetString().Should().Be("beta"); - productEntry.GetProperty("version").GetString().Should().Be("2.0+"); + json.Should().MatchInlineJsonSnapshot( + """ + { + "applies_to": [ + { + "type": "product", + "sub_type": "product", + "lifecycle": "beta", + "version": "2.0+" + } + ], + "content_hash": "", + "content_last_updated": "0001-01-01T00:00:00+00:00", + "title": "Product Test", + "search_title": "Product Test", + "content_type": "doc", + "path": "/test/product", + "url": "/test/product", + "hash": "", + "batch_index_date": "0001-01-01T00:00:00+00:00", + "last_updated": "0001-01-01T00:00:00+00:00", + "headings": [], + "parents": [], + "content_tier": "reference", + "navigation": { + "depth": 50, + "table_of_contents": 50 + } + } + """ + ); } [Test] @@ -198,26 +266,43 @@ public void SerializeDocumentWithProductApplicabilityProducesCorrectJson() }; var json = JsonSerializer.Serialize(doc, _options); - var jsonDoc = JsonDocument.Parse(json); - var root = jsonDoc.RootElement; - - root.TryGetProperty("applies_to", out var appliesTo).Should().BeTrue(); - var appliesArray = appliesTo.EnumerateArray().ToList(); - appliesArray.Should().HaveCount(2); - - // Verify apm-agent-dotnet entry - var dotnetEntry = appliesArray.FirstOrDefault(e => e.GetProperty("sub_type").GetString() == "apm-agent-dotnet"); - dotnetEntry.ValueKind.Should().NotBe(JsonValueKind.Undefined); - dotnetEntry.GetProperty("type").GetString().Should().Be("product"); - dotnetEntry.GetProperty("lifecycle").GetString().Should().Be("ga"); - dotnetEntry.GetProperty("version").GetString().Should().Be("1.5+"); - - // Verify apm-agent-node entry - var nodeEntry = appliesArray.FirstOrDefault(e => e.GetProperty("sub_type").GetString() == "apm-agent-node"); - nodeEntry.ValueKind.Should().NotBe(JsonValueKind.Undefined); - nodeEntry.GetProperty("type").GetString().Should().Be("product"); - nodeEntry.GetProperty("lifecycle").GetString().Should().Be("deprecated"); - nodeEntry.GetProperty("version").GetString().Should().Be("2.0+"); + json.Should().MatchInlineJsonSnapshot( + """ + { + "applies_to": [ + { + "type": "product", + "sub_type": "apm-agent-dotnet", + "lifecycle": "ga", + "version": "1.5+" + }, + { + "type": "product", + "sub_type": "apm-agent-node", + "lifecycle": "deprecated", + "version": "2.0+" + } + ], + "content_hash": "", + "content_last_updated": "0001-01-01T00:00:00+00:00", + "title": "APM Test", + "search_title": "APM Test", + "content_type": "doc", + "path": "/test/apm", + "url": "/test/apm", + "hash": "", + "batch_index_date": "0001-01-01T00:00:00+00:00", + "last_updated": "0001-01-01T00:00:00+00:00", + "headings": [], + "parents": [], + "content_tier": "reference", + "navigation": { + "depth": 50, + "table_of_contents": 50 + } + } + """ + ); } [Test] @@ -240,17 +325,49 @@ public void SerializeDocumentWithComplexAppliesToProducesCorrectJson() }; var json = JsonSerializer.Serialize(doc, _options); - var jsonDoc = JsonDocument.Parse(json); - var root = jsonDoc.RootElement; - - root.TryGetProperty("applies_to", out var appliesTo).Should().BeTrue(); - var appliesArray = appliesTo.EnumerateArray().ToList(); - appliesArray.Should().HaveCount(3); - - // Verify we have all three types - appliesArray.Should().Contain(e => e.GetProperty("type").GetString() == "stack"); - appliesArray.Should().Contain(e => e.GetProperty("type").GetString() == "deployment"); - appliesArray.Should().Contain(e => e.GetProperty("type").GetString() == "serverless"); + json.Should().MatchInlineJsonSnapshot( + """ + { + "applies_to": [ + { + "type": "stack", + "sub_type": "stack", + "lifecycle": "ga", + "version": "8.0+" + }, + { + "type": "deployment", + "sub_type": "ess", + "lifecycle": "ga", + "version": "all" + }, + { + "type": "serverless", + "sub_type": "elasticsearch", + "lifecycle": "ga", + "version": "all" + } + ], + "content_hash": "", + "content_last_updated": "0001-01-01T00:00:00+00:00", + "title": "Complex Test", + "search_title": "Complex Test", + "content_type": "doc", + "path": "/test/complex", + "url": "/test/complex", + "hash": "", + "batch_index_date": "0001-01-01T00:00:00+00:00", + "last_updated": "0001-01-01T00:00:00+00:00", + "headings": [], + "parents": [], + "content_tier": "reference", + "navigation": { + "depth": 50, + "table_of_contents": 50 + } + } + """ + ); } [Test] @@ -266,18 +383,29 @@ public void SerializeDocumentWithNullAppliesToOmitsField() }; var json = JsonSerializer.Serialize(doc, _options); - var jsonDoc = JsonDocument.Parse(json); - var root = jsonDoc.RootElement; - - // With default JSON options, null values might be omitted or serialized as null - // Let's check both possibilities - if (root.TryGetProperty("applies_to", out var appliesTo)) - appliesTo.ValueKind.Should().Be(JsonValueKind.Null); - else - { - // Field is omitted, which is also acceptable - true.Should().BeTrue(); - } + json.Should().MatchInlineJsonSnapshot( + """ + { + "content_hash": "", + "content_last_updated": "0001-01-01T00:00:00+00:00", + "title": "No Applies Test", + "search_title": "No Applies Test", + "content_type": "doc", + "path": "/test/no-applies", + "url": "/test/no-applies", + "hash": "", + "batch_index_date": "0001-01-01T00:00:00+00:00", + "last_updated": "0001-01-01T00:00:00+00:00", + "headings": [], + "parents": [], + "content_tier": "reference", + "navigation": { + "depth": 50, + "table_of_contents": 50 + } + } + """ + ); } [Test] @@ -293,12 +421,30 @@ public void SerializeDocumentWithEmptyAppliesToProducesEmptyArray() }; var json = JsonSerializer.Serialize(doc, _options); - var jsonDoc = JsonDocument.Parse(json); - var root = jsonDoc.RootElement; - - root.TryGetProperty("applies_to", out var appliesTo).Should().BeTrue(); - appliesTo.ValueKind.Should().Be(JsonValueKind.Array); - appliesTo.GetArrayLength().Should().Be(0); + json.Should().MatchInlineJsonSnapshot( + """ + { + "applies_to": [], + "content_hash": "", + "content_last_updated": "0001-01-01T00:00:00+00:00", + "title": "Empty Applies Test", + "search_title": "Empty Applies Test", + "content_type": "doc", + "path": "/test/empty-applies", + "url": "/test/empty-applies", + "hash": "", + "batch_index_date": "0001-01-01T00:00:00+00:00", + "last_updated": "0001-01-01T00:00:00+00:00", + "headings": [], + "parents": [], + "content_tier": "reference", + "navigation": { + "depth": 50, + "table_of_contents": 50 + } + } + """ + ); } [Test] @@ -411,22 +557,49 @@ public void SerializeDocumentWithMultipleApplicabilitiesPerTypeProducesMultipleA }; var json = JsonSerializer.Serialize(doc, _options); - var jsonDoc = JsonDocument.Parse(json); - var root = jsonDoc.RootElement; - - root.TryGetProperty("applies_to", out var appliesTo).Should().BeTrue(); - var appliesArray = appliesTo.EnumerateArray().ToList(); - appliesArray.Should().HaveCount(3); - - // All should be stack type - appliesArray.Should().OnlyContain(e => e.GetProperty("type").GetString() == "stack"); - appliesArray.Should().OnlyContain(e => e.GetProperty("sub_type").GetString() == "stack"); - - // Verify different lifecycle values - var lifecycles = appliesArray.Select(e => e.GetProperty("lifecycle").GetString()).ToList(); - lifecycles.Should().Contain("ga"); - lifecycles.Should().Contain("beta"); - lifecycles.Should().Contain("deprecated"); + json.Should().MatchInlineJsonSnapshot( + """ + { + "applies_to": [ + { + "type": "stack", + "sub_type": "stack", + "lifecycle": "ga", + "version": "8.0+" + }, + { + "type": "stack", + "sub_type": "stack", + "lifecycle": "beta", + "version": "7.17+" + }, + { + "type": "stack", + "sub_type": "stack", + "lifecycle": "deprecated", + "version": "7.0+" + } + ], + "content_hash": "", + "content_last_updated": "0001-01-01T00:00:00+00:00", + "title": "Multiple Test", + "search_title": "Multiple Test", + "content_type": "doc", + "path": "/test/multiple", + "url": "/test/multiple", + "hash": "", + "batch_index_date": "0001-01-01T00:00:00+00:00", + "last_updated": "0001-01-01T00:00:00+00:00", + "headings": [], + "parents": [], + "content_tier": "reference", + "navigation": { + "depth": 50, + "table_of_contents": 50 + } + } + """ + ); } [Test] @@ -441,13 +614,30 @@ public void SerializeDocument_IncludesSourceUrlAsSnakeCaseKeyword() }; var json = JsonSerializer.Serialize(doc, _options); - using var jsonDoc = JsonDocument.Parse(json); - jsonDoc - .RootElement - .GetProperty("source_url") - .GetString() - .Should() - .Be("https://github.com/elastic/docs-content/blob/main/docs/some-page.md"); + json.Should().MatchInlineJsonSnapshot( + """ + { + "content_hash": "", + "content_last_updated": "0001-01-01T00:00:00+00:00", + "source_url": "https://github.com/elastic/docs-content/blob/main/docs/some-page.md", + "title": "Some Page", + "search_title": "Some Page", + "content_type": "docs", + "path": "/docs/some-page", + "url": "/docs/some-page", + "hash": "", + "batch_index_date": "0001-01-01T00:00:00+00:00", + "last_updated": "0001-01-01T00:00:00+00:00", + "headings": [], + "parents": [], + "content_tier": "reference", + "navigation": { + "depth": 50, + "table_of_contents": 50 + } + } + """ + ); } [Test] @@ -456,7 +646,28 @@ public void SerializeDocument_OmitsSourceUrlWhenNull() var doc = new DocumentationDocument { Path = "/docs/some-page", Title = "Some Page", SearchTitle = "Some Page", SourceUrl = null }; var json = JsonSerializer.Serialize(doc, _options); - using var jsonDoc = JsonDocument.Parse(json); - jsonDoc.RootElement.TryGetProperty("source_url", out _).Should().BeFalse(); + json.Should().MatchInlineJsonSnapshot( + """ + { + "content_hash": "", + "content_last_updated": "0001-01-01T00:00:00+00:00", + "title": "Some Page", + "search_title": "Some Page", + "content_type": "docs", + "path": "/docs/some-page", + "url": "/docs/some-page", + "hash": "", + "batch_index_date": "0001-01-01T00:00:00+00:00", + "last_updated": "0001-01-01T00:00:00+00:00", + "headings": [], + "parents": [], + "content_tier": "reference", + "navigation": { + "depth": 50, + "table_of_contents": 50 + } + } + """ + ); } } diff --git a/tests/Elastic.Markdown.Tests/SettingsInclusion/IncludeTests.cs b/tests/Elastic.Markdown.Tests/SettingsInclusion/IncludeTests.cs index f08701fe27..c89d4e4498 100644 --- a/tests/Elastic.Markdown.Tests/SettingsInclusion/IncludeTests.cs +++ b/tests/Elastic.Markdown.Tests/SettingsInclusion/IncludeTests.cs @@ -6,8 +6,11 @@ using AwesomeAssertions; using Elastic.Documentation.Configuration; using Elastic.Documentation.Diagnostics; +using Elastic.Documentation.Testing; using Elastic.Markdown.Myst.Directives.Settings; using Elastic.Markdown.Tests.Directives; +using Nullean.Snapshots; +using Nullean.Snapshots.Html; namespace Elastic.Markdown.Tests.SettingsInclusion; @@ -43,7 +46,7 @@ public void HasNoErrors() } [Test] - public void IncludesInclusionHtml() => Html.Should().Contain("xpack.encryptedSavedObjects.encryptionKey"); + public void IncludesInclusionHtml() => Html.Should().MatchHtmlSnapshot(DocsHtml.Body()); } [InheritsTests] @@ -104,27 +107,7 @@ protected override void AddToFileSystem(MockFileSystem fileSystem) } [Test] - public void RendersAppliesToAndMetadata() - { - Html.Should().Contain("applies-to-popover"); - Html.Should().Contain("settings-supported-on"); - Html.Should().Contain("Datatype:"); - Html.Should().Contain("Default:"); - Html.Should().Contain("Options:"); - } - - [Test] - public void RendersNestedSettingName() => Html.Should().Contain("xpack.actions.customHostSettings[n].url"); - - [Test] - public void DotsInSettingNamesAreHyphensInAnchors() - { - Html.Should().Contain("id=\"xpack-actions-customhostsettings\""); - Html.Should().NotContain("id=\"xpack.actions.customhostsettings\""); - } - - [Test] - public void NestedSettingAnchorIncludesParentPrefix() => Html.Should().Contain("id=\"xpack-actions-customhostsettingsn-url\""); + public void GeneratesHtml() => Html.Should().MatchHtmlSnapshot(DocsHtml.Body()); } [InheritsTests] @@ -156,11 +139,28 @@ protected override void AddToFileSystem(MockFileSystem fileSystem) } [Test] - public void RendersAsFencedCodeBlock() - { - Html.Should().Contain("language-yaml"); - Html.Should().NotContain("[source,yaml]"); - } + public void RendersAsFencedCodeBlock() => + Html.Should().MatchInlineHtmlSnapshot( + """ +

      Example

      +
      +
      xpack.legacy.example +
      +
      +

      Example code:

      +
      +
      +
      +							xpack.legacy.example:   enabled: true
      +							
      +						
      +
      +
      +
      +
      + """, + DocsHtml.Body() + ); } [InheritsTests] @@ -193,13 +193,7 @@ Read the [preconfigured connectors](/reference/connectors-kibana/pre-configured- } [Test] - public void RendersPageDescriptionNotesAndInterpolatedGroupTitle() - { - Html.Should().Contain("General Kibana settings"); - Html.Should().Contain("Top-level note for Kibana."); - Html.Should().Contain("Group-level note for Kibana."); - Html.Should().Contain("https://www.elastic.co/docs/reference/kibana/connectors-kibana/pre-configured-connectors"); - } + public void RendersPageDescriptionNotesAndInterpolatedGroupTitle() => Html.Should().MatchHtmlSnapshot(DocsHtml.Body()); } [InheritsTests] @@ -286,10 +280,7 @@ protected override void AddToFileSystem(MockFileSystem fileSystem) } [Test] - public void ShowsEchGaSetting() => Html.Should().Contain("execution_context.enabled"); - - [Test] - public void HidesEchUnavailableSetting() => Html.Should().NotContain("console.ui.enabled"); + public void GeneratesHtml() => Html.Should().MatchHtmlSnapshot(DocsHtml.Body()); } /// @@ -331,13 +322,34 @@ protected override void AddToFileSystem(MockFileSystem fileSystem) } [Test] - public void HidesSettingWhenEchIsMissing() => Html.Should().NotContain("self.only.setting"); - - [Test] - public void ShowsSettingWithExplicitEchGa() => Html.Should().Contain("ech.explicit.setting"); - - [Test] - public void ShowsSettingWithNoAppliesTo() => Html.Should().Contain("no.applies.to.setting"); + public void GeneratesHtml() => + Html.Should().MatchInlineHtmlSnapshot( + """ +

      Example

      +
      +
      ech.explicit.setting + + +
      +
      +

      + Supported on: + + + + +

      +

      ECH is explicitly listed as ga.

      +
      +
      no.applies.to.setting +
      +
      +

      No applies_to at all — universally available.

      +
      +
      + """, + DocsHtml.Body() + ); } [InheritsTests] @@ -360,10 +372,151 @@ protected override void AddToFileSystem(MockFileSystem fileSystem) [Test] public void EmitsWarning() => - Collector.Diagnostics.Should().Contain(d => d.Severity == Severity.Warning && d.Message.Contains("invalid-deployment")); + Collector + .Diagnostics + .ToSnapshotText() + .Should() + .MatchInlineSnapshot( + """ + warning {repo}/docs/index.md:3 Unknown deployment filter 'invalid-deployment'. Valid values are: ech, ece, eck, self. + error {repo}/docs/testing/kibana-general-settings.yml:2 Substitution key {ecloud} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:1 Substitution key {kib} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:1 Substitution key {kib} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:1 Substitution key {kib} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:1 Substitution key {kib} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:1 Substitution key {kib} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:1 Substitution key {es} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:1 Substitution key {kib} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:1 Substitution key {kib} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:1 Substitution key {kib} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:1 Substitution key {kib} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:1 Substitution key {kib} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:1 Substitution key {es} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:1 Substitution key {es} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:3 Substitution key {es} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:1 Substitution key {es} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:1 Substitution key {kib} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:1 Substitution key {es} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:1 Substitution key {kib} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:1 Substitution key {es} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:1 Substitution key {es} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:1 Substitution key {kib} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:1 Substitution key {es} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:2 Substitution key {ecloud} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:1 Substitution key {es} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:1 Substitution key {es} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:1 Substitution key {es} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:1 Substitution key {es} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:2 Substitution key {ecloud} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:1 Substitution key {es} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:1 Substitution key {es} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:1 Substitution key {kib} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:1 Substitution key {es} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:1 Substitution key {kib} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:1 Substitution key {es} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:1 Substitution key {kib} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:1 Substitution key {es} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:1 Substitution key {es} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:1 Substitution key {kib} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:1 Substitution key {kib} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:1 Substitution key {kib} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:1 Substitution key {es} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:1 Substitution key {kib} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:1 Substitution key {es} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:1 Substitution key {kib} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:1 Substitution key {kib} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:1 Substitution key {kib} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:1 Substitution key {es} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:1 Substitution key {kib} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:1 Substitution key {es} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:1 Substitution key {kib} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:1 Substitution key {kib} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:1 Substitution key {kib} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:1 Substitution key {es} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:1 Substitution key {es} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:2 Substitution key {es} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:2 Substitution key {kib} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:1 Substitution key {kib} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:1 Substitution key {es} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:1 Substitution key {es} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:1 Substitution key {kib} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:1 Substitution key {kib} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:1 Substitution key {es} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:1 Substitution key {es} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:1 Substitution key {kib} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:1 Substitution key {es} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:1 Substitution key {kib} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:1 Substitution key {es} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:1 Substitution key {kib} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:1 Substitution key {es} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:1 Substitution key {es} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:1 Substitution key {kib} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:3 Substitution key {es} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:3 Substitution key {kib} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:3 Substitution key {es} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:1 Substitution key {es} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:1 Substitution key {kib} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:1 Substitution key {es} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:1 Substitution key {kib} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:1 Substitution key {es} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:1 Substitution key {hosted-ems} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:1 Substitution key {kib} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:1 Substitution key {kib} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:1 Substitution key {kib} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:2 Substitution key {ecloud} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:2 Substitution key {ecloud} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:1 Substitution key {kib} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:1 Substitution key {kib} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:1 Substitution key {kib} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:1 Substitution key {es} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:2 Substitution key {kib} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:2 Substitution key {kib} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:1 Substitution key {kib} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:1 Substitution key {kib} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:1 Substitution key {kib} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:1 Substitution key {kib} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:1 Substitution key {kib} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:1 Substitution key {kib} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:1 Substitution key {kib} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:1 Substitution key {kib} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:1 Substitution key {kib} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:1 Substitution key {kib} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:2 Substitution key {ecloud} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:1 Substitution key {kib} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:1 Substitution key {kib} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:3 Substitution key {kib} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:9 Substitution key {kib} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:1 Substitution key {kib} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:1 Substitution key {kib} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:1 Substitution key {kib} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:1 Substitution key {kib} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:1 Substitution key {kib} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:1 Substitution key {kib} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:1 Substitution key {kib} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:2 Substitution key {ecloud} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:1 Substitution key {kib} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:1 Substitution key {kib} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:1 Substitution key {kib} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:1 Substitution key {kib} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:1 Substitution key {kib} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:1 Substitution key {kib} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:1 Substitution key {kib} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:1 Substitution key {kib} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:1 Substitution key {kib} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:1 Substitution key {kib} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:1 Substitution key {kib} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:1 Substitution key {kib} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:1 Substitution key {kib} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:1 Substitution key {kib} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:6 Substitution key {ecloud} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:1 Substitution key {kib} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:1 Substitution key {es} is undefined + error {repo}/docs/testing/kibana-general-settings.yml:2 Substitution key {ecloud} is undefined + """ + ); [Test] - public void StillRendersAllSettingsWhenFilterIsInvalid() => Html.Should().Contain("execution_context.enabled"); + public void StillRendersAllSettingsWhenFilterIsInvalid() => Html.Should().MatchHtmlSnapshot(DocsHtml.Body()); } [InheritsTests] @@ -394,12 +547,34 @@ protected override void AddToFileSystem(MockFileSystem fileSystem) } [Test] - public void RendersAppliesToRoleAsBadgeNotLiteralText() - { - Html.Should().Contain("applies-to-popover"); - Html.Should().NotContain("Applies to (stack: ga 9.2)"); - Html.Should().NotContain("Applies to (stack: ga 9.1)"); - } + public void RendersAppliesToRoleAsBadgeNotLiteralText() => + Html.Should().MatchInlineHtmlSnapshot( + """ +

      Example

      +
      +
      xpack.sample.versioned +
      +
      +

      Behavior depends on version:

      +
        +
      • + + + + Defaults to + model-a.
      • +
      • + + + + Defaults to + model-b.
      • +
      +
      +
      + """, + DocsHtml.Body() + ); } /// @@ -437,21 +612,23 @@ protected override void AddToFileSystem(MockFileSystem fileSystem) } [Test] - public void RendersPlannedStackBadge() => Html.Should().Contain("badge-key=\"Stack\"").And.Contain("Planned"); - - [Test] - public void DoesNotRenderSupportedOnLine() - { - Html.Should().NotContain("settings-supported-on"); - Html.Should().NotContain("Supported on:"); - } - - [Test] - public void DoesNotRenderEchOrSelfManagedBadges() - { - Html.Should().NotContain("badge-key=\"ECH\""); - Html.Should().NotContain("badge-key=\"Self-managed\""); - } + public void GeneratesHtml() => + Html.Should().MatchInlineHtmlSnapshot( + """ +

      Example

      +
      +
      i18n.defaultLocale + + + +
      +
      +

      Locale setting planned for a future stack version.

      +
      +
      + """, + DocsHtml.Body() + ); } /// @@ -486,12 +663,30 @@ protected override void AddToFileSystem(MockFileSystem fileSystem) } [Test] - public void RendersSupportedOnLineWithBothBadges() - { - Html.Should().Contain("settings-supported-on"); - Html.Should().Contain("badge-key=\"ECH\""); - Html.Should().Contain("badge-key=\"Self-managed\""); - } + public void RendersSupportedOnLineWithBothBadges() => + Html.Should().MatchInlineHtmlSnapshot( + """ +

      Example

      +
      +
      xpack.sample.released + + + +
      +
      +

      + Supported on: + + + + +

      +

      A setting that has been GA since 7.0.

      +
      +
      + """, + DocsHtml.Body() + ); } /// @@ -527,12 +722,30 @@ protected override void AddToFileSystem(MockFileSystem fileSystem) } [Test] - public void RendersSupportedOnLine() - { - Html.Should().Contain("settings-supported-on"); - Html.Should().Contain("badge-key=\"ECH\""); - Html.Should().Contain("badge-key=\"Self-managed\""); - } + public void RendersSupportedOnLine() => + Html.Should().MatchInlineHtmlSnapshot( + """ +

      Example

      +
      +
      xpack.sample.mixed + + + +
      +
      +

      + Supported on: + + + + +

      +

      GA since an existing version, scheduled for deprecation in a future version.

      +
      +
      + """, + DocsHtml.Body() + ); } /// @@ -569,12 +782,8 @@ protected override void AddToFileSystem(MockFileSystem fileSystem) } [Test] - public void HasNoErrors() => Collector.Diagnostics.Should().BeEmpty(); + public void HasNoErrors() => Collector.Diagnostics.ToSnapshotText().Should().MatchInlineSnapshot(""); [Test] - public void RendersBothSettings() - { - Html.Should().Contain("xpack.sample.one"); - Html.Should().Contain("xpack.sample.two"); - } + public void RendersBothSettings() => Html.Should().MatchHtmlSnapshot(DocsHtml.Body()); } diff --git a/tests/Elastic.Markdown.Tests/TaskList/BasicTaskListTests.cs b/tests/Elastic.Markdown.Tests/TaskList/BasicTaskListTests.cs index 3c85e32a17..e7482918fd 100644 --- a/tests/Elastic.Markdown.Tests/TaskList/BasicTaskListTests.cs +++ b/tests/Elastic.Markdown.Tests/TaskList/BasicTaskListTests.cs @@ -3,7 +3,9 @@ // See the LICENSE file in the project root for more information using AwesomeAssertions; +using Elastic.Documentation.Testing; using Elastic.Markdown.Tests.Inline; +using Nullean.Snapshots.Html; namespace Elastic.Markdown.Tests.TaskList; @@ -13,14 +15,50 @@ public class BasicTaskListTests() : InlineTest(""" """) { [Test] - public void RendersTaskListContainer() => Html.Should().Contain("class=\"contains-task-list\""); + public void GeneratesHtml() => + Html.Should().MatchInlineHtmlSnapshot( + """ +
        +
      • + + A pending task
      • +
      • + + A completed task
      • +
      + """, + DocsHtml.Body() + ); [Test] - public void RendersTaskListItem() => Html.Should().Contain("class=\"task-list-item\""); + public void RendersUncheckedCheckbox() => + Html.Should().MatchInlineHtmlSnapshot( + """ +
        +
      • + + A pending task
      • +
      • + + A completed task
      • +
      + """, + DocsHtml.Body() + ); [Test] - public void RendersUncheckedCheckbox() => Html.ShouldContainHtml(""""""); - - [Test] - public void RendersCheckedCheckbox() => Html.ShouldContainHtml(""""""); + public void RendersCheckedCheckbox() => + Html.Should().MatchInlineHtmlSnapshot( + """ +
        +
      • + + A pending task
      • +
      • + + A completed task
      • +
      + """, + DocsHtml.Body() + ); } diff --git a/tests/Elastic.Markdown.Tests/Tracking/LocalChangeTrackingServiceTests.cs b/tests/Elastic.Markdown.Tests/Tracking/LocalChangeTrackingServiceTests.cs index 83b37fdad6..370be8e315 100644 --- a/tests/Elastic.Markdown.Tests/Tracking/LocalChangeTrackingServiceTests.cs +++ b/tests/Elastic.Markdown.Tests/Tracking/LocalChangeTrackingServiceTests.cs @@ -8,7 +8,9 @@ using Elastic.Documentation.Diagnostics; using Elastic.Documentation.FileSystems; using Elastic.Documentation.Refactor.Tracking; +using Elastic.Documentation.Testing; using Nullean.ScopedFileSystem; +using Nullean.Snapshots; namespace Elastic.Markdown.Tests.Tracking; @@ -51,14 +53,12 @@ public async Task DocsetAtRepoRoot_DeletedMarkdownWithoutRedirect_EmitsError() var ok = await service.ValidateRedirects(collector, source, fs); ok.Should().BeFalse(); - collector.Errors.Should().BeGreaterThan(0); collector .Diagnostics + .ToSnapshotText() .Should() - .Contain( - d => d.Severity == Severity.Error && d.Message.Contains("troubleshoot/deployments/serverless.md") && d.Message.Contains( - "redirects.yml" - ) + .MatchInlineSnapshot( + "error {temp}/diff-validate-test-{id}/redirects.yml Missing 'troubleshoot/deployments/serverless.md' in redirects.yml. 'troubleshoot/deployments/serverless.md' was deleted but it has no redirect targets. This will lead to broken links." ); } @@ -93,7 +93,13 @@ public async Task DocsetInSubfolder_DeletedMarkdownWithoutRedirect_EmitsError() var ok = await service.ValidateRedirects(collector, source, fs); ok.Should().BeFalse(); - collector.Diagnostics.Should().Contain(d => d.Severity == Severity.Error && d.Message.Contains("reference/old-page.md")); + collector + .Diagnostics + .ToSnapshotText() + .Should() + .MatchInlineSnapshot( + "error {temp}/diff-validate-test-{id}/docs/redirects.yml Missing 'reference/old-page.md' in redirects.yml. 'docs/reference/old-page.md' was deleted but it has no redirect targets. This will lead to broken links." + ); } [Test] @@ -137,9 +143,10 @@ public async Task RenamedMarkdownWithoutRedirect_EmitsError() ok.Should().BeFalse(); collector .Diagnostics + .ToSnapshotText() .Should() - .Contain( - d => d.Severity == Severity.Error && d.Message.Contains("troubleshoot/old.md") && d.Message.Contains("troubleshoot/new.md") + .MatchInlineSnapshot( + "error {temp}/diff-validate-test-{id}/redirects.yml Missing 'troubleshoot/old.md' in redirects.yml. 'troubleshoot/old.md' was renamed to 'troubleshoot/new.md' but it has no redirect configuration set." ); }