diff --git a/samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/RequestOptions.cshtml b/samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/RequestOptions.cshtml index 8cbd6a4..273593b 100644 --- a/samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/RequestOptions.cshtml +++ b/samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/RequestOptions.cshtml @@ -13,8 +13,8 @@
-

hx-request-timeout

-

The toolkit emits an HTMX 4 hx-config timeout for this report preview.

+

hx-request-*

+

The toolkit emits an HTMX 4 hx-config with a timeout, cache mode, and redirect mode for this report preview.

@@ -22,6 +22,8 @@ hx-page="/Examples/RequestOptions" hx-page-handler="Preview" hx-request-timeout="2000" + hx-request-cache="@HtmxRequestCache.NoCache" + hx-request-redirect="@HtmxRequestRedirect.Error" hx-target="#preview-result"> Generate report preview diff --git a/src/Ramstack.HtmxToolkit/Configuration/HtmxV1Config.cs b/src/Ramstack.HtmxToolkit/Configuration/HtmxV1Config.cs index d9aa4d2..1bd2dfc 100644 --- a/src/Ramstack.HtmxToolkit/Configuration/HtmxV1Config.cs +++ b/src/Ramstack.HtmxToolkit/Configuration/HtmxV1Config.cs @@ -182,16 +182,6 @@ public bool? UseTemplateFragments set => SetField(ref field, value); } - /// - /// Gets or sets the WebSocket reconnection delay strategy. - /// The HTMX default is full-jitter. - /// - public string? WsReconnectDelay - { - get; - set => SetField(ref field, value); - } - /// /// Gets or sets the type of binary data received over WebSocket connections. /// The HTMX default is . diff --git a/src/Ramstack.HtmxToolkit/Configuration/HtmxV2Config.cs b/src/Ramstack.HtmxToolkit/Configuration/HtmxV2Config.cs index ddcceae..ea56299 100644 --- a/src/Ramstack.HtmxToolkit/Configuration/HtmxV2Config.cs +++ b/src/Ramstack.HtmxToolkit/Configuration/HtmxV2Config.cs @@ -182,17 +182,6 @@ public string[]? AttributesToSettle set => SetField(ref field, value); } - /// - /// Gets or sets the WebSocket reconnection delay strategy. - /// The HTMX default is full-jitter. - /// - /// Requires the HTMX WebSocket extension. - public string? WsReconnectDelay - { - get; - set => SetField(ref field, value); - } - /// /// Gets or sets the type of binary data received over WebSocket connections. /// The HTMX default is . diff --git a/src/Ramstack.HtmxToolkit/Configuration/HtmxV4Config.cs b/src/Ramstack.HtmxToolkit/Configuration/HtmxV4Config.cs index ca3bb1f..3ffeab4 100644 --- a/src/Ramstack.HtmxToolkit/Configuration/HtmxV4Config.cs +++ b/src/Ramstack.HtmxToolkit/Configuration/HtmxV4Config.cs @@ -121,9 +121,13 @@ public string? InlineScriptNonce } /// - /// Gets or sets a comma-separated list of extensions that HTMX is allowed to load. - /// The HTMX default is an empty string. + /// Gets or sets a comma-separated list of extensions that HTMX is allowed to register. + /// The HTMX default is an empty string, which allows all extensions. /// + /// + /// HTMX compares extension names exactly. Whitespace is allowed only after a comma; + /// any other whitespace becomes part of a name, so "sse ,ws" does not allow sse. + /// public string? Extensions { get; diff --git a/src/Ramstack.HtmxToolkit/Configuration/ResponseHandlingConfig.cs b/src/Ramstack.HtmxToolkit/Configuration/ResponseHandlingConfig.cs index 6383234..1d74066 100644 --- a/src/Ramstack.HtmxToolkit/Configuration/ResponseHandlingConfig.cs +++ b/src/Ramstack.HtmxToolkit/Configuration/ResponseHandlingConfig.cs @@ -1,3 +1,7 @@ +using System.Text.Json.Serialization; + +using Ramstack.HtmxToolkit.Internal; + namespace Ramstack.HtmxToolkit.Configuration; /// @@ -37,7 +41,23 @@ public sealed class ResponseHandlingConfig public string? Target { get; set; } /// - /// Gets or sets an alternative swap mechanism for the response. + /// Gets or sets an alternative swap style for the response. + /// + /// + /// Reads the style from , or returns + /// if the style is not recognized. Assigning a style replaces the complete expression, + /// including any modifiers. Assigning clears the override. + /// + [JsonIgnore] + public HtmxSwap? SwapOverride + { + get => EnumHelper.ParseHtmxSwap(SwapOverrideExpression); + set => SwapOverrideExpression = value.GetSwapValue(); + } + + /// + /// Gets or sets the complete swap override expression, including any swap modifiers. /// - public string? SwapOverride { get; set; } + [JsonPropertyName("swapOverride")] + public string? SwapOverrideExpression { get; set; } } diff --git a/src/Ramstack.HtmxToolkit/HtmxLocationOptions.cs b/src/Ramstack.HtmxToolkit/HtmxLocationOptions.cs index 25bb455..c5d64df 100644 --- a/src/Ramstack.HtmxToolkit/HtmxLocationOptions.cs +++ b/src/Ramstack.HtmxToolkit/HtmxLocationOptions.cs @@ -1,3 +1,5 @@ +using Ramstack.HtmxToolkit.Internal; + namespace Ramstack.HtmxToolkit; /// @@ -20,8 +22,25 @@ public sealed class HtmxLocationOptions /// /// Gets or sets how the response will be swapped relative to the target element. /// + /// + /// Supported in HTMX 1.9.x, HTMX 2.x, and HTMX 4.x. + /// + /// Reads the style from , or returns + /// if the style is not recognized. Assigning a style replaces the complete expression, + /// including any modifiers. Assigning clears the swap. + /// + /// + public HtmxSwap? Swap + { + get => EnumHelper.ParseHtmxSwap(SwapExpression); + set => SwapExpression = value.GetSwapValue(); + } + + /// + /// Gets or sets the complete swap expression, including any swap modifiers. + /// /// Supported in HTMX 1.9.x, HTMX 2.x, and HTMX 4.x. - public HtmxSwap? Swap { get; set; } + public string? SwapExpression { get; set; } /// /// Gets or sets the form field values to submit with the request. diff --git a/src/Ramstack.HtmxToolkit/HtmxRequestCache.cs b/src/Ramstack.HtmxToolkit/HtmxRequestCache.cs new file mode 100644 index 0000000..7ed96bf --- /dev/null +++ b/src/Ramstack.HtmxToolkit/HtmxRequestCache.cs @@ -0,0 +1,42 @@ +namespace Ramstack.HtmxToolkit; + +/// +/// Specifies the Fetch cache mode for an HTMX request. +/// +/// Supported only in HTMX 4.x. +public enum HtmxRequestCache +{ + /// + /// Uses the browser's normal HTTP cache behavior. + /// + Default, + + /// + /// Bypasses the cache and does not store the response in it. + /// + NoStore, + + /// + /// Bypasses the cache lookup and updates the cache with the response. + /// + Reload, + + /// + /// Revalidates a cached response before using it. + /// + NoCache, + + /// + /// Uses a cached response even if stale, or fetches and caches a response on a cache miss. + /// + ForceCache, + + /// + /// Uses a cached response even if stale, and fails on a cache miss. + /// + /// + /// Requires the same-origin Fetch mode, which is the HTMX default. + /// The request fails if specifies another mode. + /// + OnlyIfCached +} diff --git a/src/Ramstack.HtmxToolkit/HtmxRequestRedirect.cs b/src/Ramstack.HtmxToolkit/HtmxRequestRedirect.cs new file mode 100644 index 0000000..a705a40 --- /dev/null +++ b/src/Ramstack.HtmxToolkit/HtmxRequestRedirect.cs @@ -0,0 +1,31 @@ +namespace Ramstack.HtmxToolkit; + +/// +/// Specifies the Fetch redirect mode for an HTMX request. +/// +/// Supported only in HTMX 4.x. +public enum HtmxRequestRedirect +{ + /// + /// Follows redirects automatically. + /// + Follow, + + /// + /// Fails the request when a redirect is encountered. + /// + /// + /// HTMX raises the htmx:error event and does not swap the response. + /// + Error, + + /// + /// Returns an opaque redirect response instead of following the redirect. + /// + /// + /// The response has status 0, and its headers and body are not accessible. + /// HTMX cannot read HX-* response headers, does not treat the response as an error, + /// and swaps the empty body into the target. + /// + Manual +} diff --git a/src/Ramstack.HtmxToolkit/HtmxResponse.cs b/src/Ramstack.HtmxToolkit/HtmxResponse.cs index 8885b63..88fe4a0 100644 --- a/src/Ramstack.HtmxToolkit/HtmxResponse.cs +++ b/src/Ramstack.HtmxToolkit/HtmxResponse.cs @@ -197,10 +197,10 @@ public HtmxResponse Reselect(string value) => SetHeader(this, HtmxResponseHeaderNames.Reselect, value); /// - /// Adds a client-side event to the response header selected by . + /// Adds a client-side event to the response header selected by . /// /// The event name to trigger. - /// The event timing. Defaults to . + /// The event timing. Defaults to . /// /// The current instance. /// @@ -209,8 +209,8 @@ public HtmxResponse Reselect(string value) => /// HX-Trigger and runs when the request completes (after the swap whenever one is performed). /// See PR #3900. /// - public HtmxResponse TriggerEvent(string eventName, HtmxTriggerTiming trigger = HtmxTriggerTiming.Receive) => - AddToPendingEvent(this, eventName, "{}", trigger); + public HtmxResponse TriggerEvent(string eventName, HtmxTriggerTiming timing = HtmxTriggerTiming.Receive) => + AddToPendingEvent(this, eventName, "{}", timing); /// /// Adds a client-side event and its detail to the response header selected by diff --git a/src/Ramstack.HtmxToolkit/Serialization/HtmxLocationOptionsJsonView.cs b/src/Ramstack.HtmxToolkit/Serialization/HtmxLocationOptionsJsonView.cs index 26df092..4fae8cd 100644 --- a/src/Ramstack.HtmxToolkit/Serialization/HtmxLocationOptionsJsonView.cs +++ b/src/Ramstack.HtmxToolkit/Serialization/HtmxLocationOptionsJsonView.cs @@ -25,10 +25,9 @@ internal readonly struct HtmxLocationOptionsJsonView(string path, HtmxLocationOp public string? Target => options.Target; /// - /// Gets the serialized swap style. + /// Gets the swap expression. /// - [JsonConverter(typeof(HtmxSwapJsonConverter))] - public HtmxSwap? Swap => options.Swap; + public string? Swap => options.SwapExpression; /// /// Gets the form field values. diff --git a/src/Ramstack.HtmxToolkit/TagHelpers/HtmxRequestTagHelper.cs b/src/Ramstack.HtmxToolkit/TagHelpers/HtmxRequestTagHelper.cs index e4f2692..21e5b2c 100644 --- a/src/Ramstack.HtmxToolkit/TagHelpers/HtmxRequestTagHelper.cs +++ b/src/Ramstack.HtmxToolkit/TagHelpers/HtmxRequestTagHelper.cs @@ -121,7 +121,7 @@ public bool? NoHeaders /// /// Supported only in HTMX 4.x. [HtmlAttributeName(RequestCacheAttributeName)] - public string? Cache + public HtmxRequestCache? Cache { get => _request.Cache; set => _request.Cache = value; @@ -132,7 +132,7 @@ public string? Cache /// /// Supported only in HTMX 4.x. [HtmlAttributeName(RequestRedirectAttributeName)] - public string? Redirect + public HtmxRequestRedirect? Redirect { get => _request.Redirect; set => _request.Redirect = value; @@ -217,10 +217,10 @@ internal sealed class HtmxRequestData public bool? NoHeaders { get; set; } /// - public string? Cache { get; set; } + public HtmxRequestCache? Cache { get; set; } /// - public string? Redirect { get; set; } + public HtmxRequestRedirect? Redirect { get; set; } /// public string? Referrer { get; set; } @@ -286,10 +286,25 @@ internal readonly struct HtmxRequestDataV4(HtmxRequestData data) }; /// - public string? Cache => data.Cache; + public string? Cache => data.Cache switch + { + HtmxRequestCache.Default => "default", + HtmxRequestCache.NoStore => "no-store", + HtmxRequestCache.Reload => "reload", + HtmxRequestCache.NoCache => "no-cache", + HtmxRequestCache.ForceCache => "force-cache", + HtmxRequestCache.OnlyIfCached => "only-if-cached", + _ => null + }; /// - public string? Redirect => data.Redirect; + public string? Redirect => data.Redirect switch + { + HtmxRequestRedirect.Follow => "follow", + HtmxRequestRedirect.Error => "error", + HtmxRequestRedirect.Manual => "manual", + _ => null + }; /// public string? Referrer => data.Referrer; diff --git a/tests/Ramstack.HtmxToolkit.Tests/HtmxResponseTests.cs b/tests/Ramstack.HtmxToolkit.Tests/HtmxResponseTests.cs index 05f6d5e..d6b1aea 100644 --- a/tests/Ramstack.HtmxToolkit.Tests/HtmxResponseTests.cs +++ b/tests/Ramstack.HtmxToolkit.Tests/HtmxResponseTests.cs @@ -54,6 +54,67 @@ public void Location_WithOptions_OmitsNullProperties() Assert.That(json.ContainsKey("swap"), Is.False); } + [TestCase("outerHTML settle:200ms", HtmxSwap.OuterHtml)] + [TestCase("customSwap settle:200ms", null)] + public void Location_WithSwapExpression_SerializesExpression(string expression, HtmxSwap? expectedStyle) + { + var context = TestHelper.CreateHtmxRequestContext(); + var options = new HtmxLocationOptions + { + Swap = HtmxSwap.InnerHtml, + SwapExpression = expression + }; + + context.Response.Htmx(r => r.Location("/bar", options)); + + var header = context.Response.Headers[HtmxResponseHeaderNames.Location].ToString(); + var json = JsonHelper.ParseJson(header); + + Assert.That(options.Swap, Is.EqualTo(expectedStyle)); + Assert.That(json["swap"].GetString(), Is.EqualTo(expression)); + } + + [Test] + public void Location_WithTypedSwap_ReplacesExpression() + { + var context = TestHelper.CreateHtmxRequestContext(); + var options = new HtmxLocationOptions + { + SwapExpression = "innerHTML settle:200ms", + Swap = HtmxSwap.OuterHtml + }; + + context.Response.Htmx(r => r.Location("/bar", options)); + + var header = context.Response.Headers[HtmxResponseHeaderNames.Location].ToString(); + var json = JsonHelper.ParseJson(header); + + Assert.That(options.SwapExpression, Is.EqualTo("outerHTML")); + Assert.That(json["swap"].GetString(), Is.EqualTo("outerHTML")); + } + + [TestCase(false)] + [TestCase(true)] + public void Location_WithClearedSwap_OmitsSwap(bool clearExpression) + { + var context = TestHelper.CreateHtmxRequestContext(); + var options = new HtmxLocationOptions { SwapExpression = "outerHTML settle:200ms" }; + + if (clearExpression) + options.SwapExpression = null; + else + options.Swap = null; + + context.Response.Htmx(r => r.Location("/bar", options)); + + var header = context.Response.Headers[HtmxResponseHeaderNames.Location].ToString(); + var json = JsonHelper.ParseJson(header); + + Assert.That(options.Swap, Is.Null); + Assert.That(options.SwapExpression, Is.Null); + Assert.That(json.ContainsKey("swap"), Is.False); + } + [Test] public void Location_WithOptions_SerializesValuesAndHeaders() { diff --git a/tests/Ramstack.HtmxToolkit.Tests/TagHelpers/HtmxConfigTagHelperTests.cs b/tests/Ramstack.HtmxToolkit.Tests/TagHelpers/HtmxConfigTagHelperTests.cs index 4bdd3c8..1b0f64f 100644 --- a/tests/Ramstack.HtmxToolkit.Tests/TagHelpers/HtmxConfigTagHelperTests.cs +++ b/tests/Ramstack.HtmxToolkit.Tests/TagHelpers/HtmxConfigTagHelperTests.cs @@ -91,7 +91,7 @@ public async Task ProcessAsync_SerializesFullResponseHandlingConfig() IgnoreTitle = true, Select = "#content", Target = "#target", - SwapOverride = "innerHTML" + SwapOverride = HtmxSwap.InnerHtml } ]); @@ -110,6 +110,84 @@ public async Task ProcessAsync_SerializesFullResponseHandlingConfig() }); } + [TestCase(HtmxSwap.InnerHtml, "innerHTML")] + [TestCase(HtmxSwap.OuterHtml, "outerHTML")] + [TestCase(HtmxSwap.None, "none")] + public async Task ProcessAsync_SerializesResponseHandlingSwapOverride(HtmxSwap swap, string expected) + { + var options = new HtmxToolkitOptions(); + options.UseHtmxV2(config => + config.ResponseHandling = [new() { SwapOverride = swap }]); + + var json = await RenderJson(options); + + Assert.That( + json["responseHandling"].GetRawText(), + Is.EqualTo("[{\"swapOverride\":\"" + expected + "\"}]")); + } + + [TestCase("outerHTML settle:200ms", HtmxSwap.OuterHtml)] + [TestCase("customSwap settle:200ms", null)] + public async Task ProcessAsync_SerializesResponseHandlingSwapOverrideExpression(string expression, HtmxSwap? expectedStyle) + { + var handlingConfig = new ResponseHandlingConfig + { + SwapOverride = HtmxSwap.InnerHtml, + SwapOverrideExpression = expression + }; + + var options = new HtmxToolkitOptions(); + options.UseHtmxV2(config => config.ResponseHandling = [handlingConfig]); + + var json = await RenderJson(options); + + Assert.That(handlingConfig.SwapOverride, Is.EqualTo(expectedStyle)); + Assert.That( + json["responseHandling"].GetRawText(), + Is.EqualTo("[{\"swapOverride\":\"" + expression + "\"}]")); + } + + [Test] + public async Task ProcessAsync_ResponseHandlingTypedSwapOverride_ReplacesExpression() + { + var handlingConfig = new ResponseHandlingConfig + { + SwapOverrideExpression = "innerHTML settle:200ms", + SwapOverride = HtmxSwap.OuterHtml + }; + + var options = new HtmxToolkitOptions(); + options.UseHtmxV2(config => config.ResponseHandling = [handlingConfig]); + + var json = await RenderJson(options); + + Assert.That(handlingConfig.SwapOverrideExpression, Is.EqualTo("outerHTML")); + Assert.That( + json["responseHandling"].GetRawText(), + Is.EqualTo("[{\"swapOverride\":\"outerHTML\"}]")); + } + + [TestCase(false)] + [TestCase(true)] + public async Task ProcessAsync_OmitsClearedResponseHandlingSwapOverride(bool clearExpression) + { + var handlingConfig = new ResponseHandlingConfig { SwapOverrideExpression = "outerHTML settle:200ms" }; + + if (clearExpression) + handlingConfig.SwapOverrideExpression = null; + else + handlingConfig.SwapOverride = null; + + var options = new HtmxToolkitOptions(); + options.UseHtmxV2(config => config.ResponseHandling = [handlingConfig]); + + var json = await RenderJson(options); + + Assert.That(handlingConfig.SwapOverride, Is.Null); + Assert.That(handlingConfig.SwapOverrideExpression, Is.Null); + Assert.That(json["responseHandling"].GetRawText(), Is.EqualTo("[{}]")); + } + [Test] public async Task ProcessAsync_SerializesOnlyHtmx1Options() { @@ -133,7 +211,6 @@ public async Task ProcessAsync_SerializesOnlyHtmx1Options() htmx.InlineScriptNonce = "script-nonce"; htmx.AttributesToSettle = ["class", "style"]; htmx.UseTemplateFragments = true; - htmx.WsReconnectDelay = "exponential"; htmx.WsBinaryType = HtmxBinaryType.ArrayBuffer; htmx.DisableSelector = "[data-disable]"; htmx.WithCredentials = true; @@ -157,7 +234,7 @@ public async Task ProcessAsync_SerializesOnlyHtmx1Options() "defaultSwapDelay", "defaultSettleDelay", "includeIndicatorStyles", "indicatorClass", "requestClass", "addedClass", "swappingClass", "settlingClass", "allowEval", "allowScriptTags", "inlineScriptNonce", "attributesToSettle", "useTemplateFragments", - "wsReconnectDelay", "wsBinaryType", "disableSelector", "withCredentials", "timeout", + "wsBinaryType", "disableSelector", "withCredentials", "timeout", "selfRequestsOnly", "scrollBehavior", "defaultFocusScroll", "getCacheBusterParam", "globalViewTransitions", "methodsThatUseUrlParams", "ignoreTitle", "scrollIntoViewOnBoost", "triggerSpecsCache" @@ -192,7 +269,6 @@ public async Task ProcessAsync_SerializesOnlyHtmx2Options_InResponseHandlingOrde htmx.InlineScriptNonce = "script-nonce"; htmx.InlineStyleNonce = "style-nonce"; htmx.AttributesToSettle = ["class", "style"]; - htmx.WsReconnectDelay = "exponential"; htmx.WsBinaryType = HtmxBinaryType.Blob; htmx.DisableSelector = "[data-disable]"; htmx.WithCredentials = true; @@ -225,7 +301,7 @@ public async Task ProcessAsync_SerializesOnlyHtmx2Options_InResponseHandlingOrde "defaultSwapDelay", "defaultSettleDelay", "includeIndicatorStyles", "indicatorClass", "requestClass", "addedClass", "swappingClass", "settlingClass", "allowEval", "allowScriptTags", "inlineScriptNonce", "inlineStyleNonce", "attributesToSettle", - "wsReconnectDelay", "wsBinaryType", "disableSelector", "withCredentials", + "wsBinaryType", "disableSelector", "withCredentials", "disableInheritance", "timeout", "selfRequestsOnly", "scrollBehavior", "defaultFocusScroll", "getCacheBusterParam", "globalViewTransitions", "methodsThatUseUrlParams", "ignoreTitle", "scrollIntoViewOnBoost", "triggerSpecsCache", "responseHandling", "allowNestedOobSwaps", diff --git a/tests/Ramstack.HtmxToolkit.Tests/TagHelpers/HtmxRequestTagHelperTests.cs b/tests/Ramstack.HtmxToolkit.Tests/TagHelpers/HtmxRequestTagHelperTests.cs index 8ce52d5..7a309dd 100644 --- a/tests/Ramstack.HtmxToolkit.Tests/TagHelpers/HtmxRequestTagHelperTests.cs +++ b/tests/Ramstack.HtmxToolkit.Tests/TagHelpers/HtmxRequestTagHelperTests.cs @@ -93,8 +93,8 @@ public async Task ProcessAsync_SerializesHtmx4RequestConfiguration() helper.Timeout = 500; helper.Credentials = HtmxRequestCredentials.Omit; helper.NoHeaders = true; - helper.Cache = "no-cache"; - helper.Redirect = "manual"; + helper.Cache = HtmxRequestCache.NoCache; + helper.Redirect = HtmxRequestRedirect.Manual; helper.Referrer = "no-referrer"; helper.Integrity = "sha384-example"; helper.Validate = true; @@ -136,6 +136,82 @@ public async Task ProcessAsync_SerializesHtmx4CredentialsMode(HtmxRequestCredent Assert.That(json["credentials"].GetString(), Is.EqualTo(expected)); } + [TestCase(HtmxRequestCache.Default, "default")] + [TestCase(HtmxRequestCache.NoStore, "no-store")] + [TestCase(HtmxRequestCache.Reload, "reload")] + [TestCase(HtmxRequestCache.NoCache, "no-cache")] + [TestCase(HtmxRequestCache.ForceCache, "force-cache")] + [TestCase(HtmxRequestCache.OnlyIfCached, "only-if-cached")] + public async Task ProcessAsync_SerializesHtmx4CacheMode(HtmxRequestCache cache, string expected) + { + var output = TestHelper.CreateTagHelperOutput(); + var helper = CreateHelper(HtmxTargetVersion.V4); + helper.Cache = cache; + + await helper.ProcessAsync(TestHelper.CreateTagHelperContext(), output); + + Assert.That(output.Attributes["hx-config"]!.Value.ToString(), Is.EqualTo("{\"cache\":\"" + expected + "\"}")); + Assert.That(output.Attributes["hx-request"], Is.Null); + } + + [TestCase(HtmxRequestRedirect.Follow, "follow")] + [TestCase(HtmxRequestRedirect.Error, "error")] + [TestCase(HtmxRequestRedirect.Manual, "manual")] + public async Task ProcessAsync_SerializesHtmx4RedirectMode(HtmxRequestRedirect redirect, string expected) + { + var output = TestHelper.CreateTagHelperOutput(); + var helper = CreateHelper(HtmxTargetVersion.V4); + helper.Redirect = redirect; + + await helper.ProcessAsync(TestHelper.CreateTagHelperContext(), output); + + Assert.That(output.Attributes["hx-config"]!.Value.ToString(), Is.EqualTo("{\"redirect\":\"" + expected + "\"}")); + Assert.That(output.Attributes["hx-request"], Is.Null); + } + + [Test] + public async Task ProcessAsync_Htmx4_OmitsClearedCacheAndRedirectModes() + { + var output = TestHelper.CreateTagHelperOutput(); + var helper = CreateHelper(HtmxTargetVersion.V4); + helper.Timeout = 500; + helper.Cache = HtmxRequestCache.Default; + helper.Redirect = HtmxRequestRedirect.Follow; + helper.Cache = null; + helper.Redirect = null; + + await helper.ProcessAsync(TestHelper.CreateTagHelperContext(), output); + + Assert.That(output.Attributes["hx-config"]!.Value.ToString(), Is.EqualTo("{\"timeout\":500}")); + } + + [TestCase(HtmxTargetVersion.V1)] + [TestCase(HtmxTargetVersion.V2)] + public async Task ProcessAsync_OmitsCacheAndRedirectModesForPriorVersions(HtmxTargetVersion version) + { + var output = TestHelper.CreateTagHelperOutput(); + var helper = CreateHelper(version); + helper.Timeout = 500; + helper.Cache = HtmxRequestCache.NoCache; + helper.Redirect = HtmxRequestRedirect.Manual; + + await helper.ProcessAsync(TestHelper.CreateTagHelperContext(), output); + + Assert.That(output.Attributes["hx-request"]!.Value.ToString(), Is.EqualTo("{\"timeout\":500}")); + Assert.That(output.Attributes["hx-config"], Is.Null); + } + + [Test] + public async Task ProcessAsync_Htmx4_OmitsUnsetConfiguration() + { + var output = TestHelper.CreateTagHelperOutput(); + var helper = CreateHelper(HtmxTargetVersion.V4); + + await helper.ProcessAsync(TestHelper.CreateTagHelperContext(), output); + + Assert.That(output.Attributes, Is.Empty); + } + [Test] public async Task ProcessAsync_OmitsHtmx4OnlyCredentialsModeForPriorVersions() {