diff --git a/.github/workflows/build.yml b/.github/workflows/test.yml
similarity index 65%
rename from .github/workflows/build.yml
rename to .github/workflows/test.yml
index 7516b48..0787c70 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/test.yml
@@ -1,4 +1,4 @@
-name: Build
+name: Build & Test
on:
pull_request:
@@ -6,14 +6,14 @@ on:
types: [opened, synchronize, reopened, ready_for_review]
jobs:
- publish:
- name: Build packages
+ build-and-test:
+ name: Build & Test
runs-on: ubuntu-latest
steps:
- name: Install .NET
uses: actions/setup-dotnet@v4
with:
- dotnet-version: 9.0.x
+ dotnet-version: 10.x
- name: Checkout
uses: actions/checkout@v4
@@ -25,3 +25,9 @@ jobs:
- name: Build (Release)
run: dotnet build -c Release
+
+ - name: Test (Debug)
+ run: dotnet test -c Debug --no-build
+
+ - name: Test (Release)
+ run: dotnet test -c Release --no-build
diff --git a/README.md b/README.md
index efba93c..37d8cc5 100644
--- a/README.md
+++ b/README.md
@@ -669,9 +669,9 @@ Alternatively, you can set the entire response handling configuration directly a
```html
```
diff --git a/Ramstack.HtmxToolkit.slnx b/Ramstack.HtmxToolkit.slnx
index 9f1b330..92f9a49 100644
--- a/Ramstack.HtmxToolkit.slnx
+++ b/Ramstack.HtmxToolkit.slnx
@@ -10,4 +10,5 @@
+
diff --git a/src/Ramstack.HtmxToolkit/PendingEvents.cs b/src/Ramstack.HtmxToolkit/PendingEvents.cs
index 38f9b9e..6943f34 100644
--- a/src/Ramstack.HtmxToolkit/PendingEvents.cs
+++ b/src/Ramstack.HtmxToolkit/PendingEvents.cs
@@ -11,14 +11,22 @@ namespace Ramstack.HtmxToolkit;
/// Accumulates htmx events per for a single request,
/// deferring header serialization until the response is about to start.
///
-internal sealed class PendingEvents(HttpResponse response)
+internal sealed class PendingEvents
{
private const string ProxyEventName = "_r_proxy";
+ private readonly HttpResponse _response;
private Dictionary? _receive;
private Dictionary? _afterSwap;
private Dictionary? _afterSettle;
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The HTTP response to which the events belong.
+ private PendingEvents(HttpResponse response) =>
+ _response = response;
+
///
/// Adds the specified events to the pending set for the given .
/// When a key already exists, the duplicate event is accumulated under a _r_proxy key and replayed client-side.
@@ -135,6 +143,6 @@ public static PendingEvents GetOrCreate(HttpResponse response)
private void SetHeader(string name, Dictionary? events)
{
if (events is not null)
- response.Headers[name] = JsonSerializer.Serialize(events, JsonOptions.CamelCase);
+ _response.Headers[name] = JsonSerializer.Serialize(events, JsonOptions.CamelCase);
}
}
diff --git a/src/Ramstack.HtmxToolkit/Ramstack.HtmxToolkit.csproj b/src/Ramstack.HtmxToolkit/Ramstack.HtmxToolkit.csproj
index bfb969f..f4d69ca 100644
--- a/src/Ramstack.HtmxToolkit/Ramstack.HtmxToolkit.csproj
+++ b/src/Ramstack.HtmxToolkit/Ramstack.HtmxToolkit.csproj
@@ -55,6 +55,10 @@
+
+
+
+
diff --git a/src/Ramstack.HtmxToolkit/TagHelpers/HtmxConfigTagHelper.cs b/src/Ramstack.HtmxToolkit/TagHelpers/HtmxConfigTagHelper.cs
index 763834f..db3ef07 100644
--- a/src/Ramstack.HtmxToolkit/TagHelpers/HtmxConfigTagHelper.cs
+++ b/src/Ramstack.HtmxToolkit/TagHelpers/HtmxConfigTagHelper.cs
@@ -432,12 +432,12 @@ public string? TriggerSpecsCache
///
/// Gets or sets the default response handling behavior for HTTP response status codes.
- /// Accepts an array of objects that define
+ /// Accepts an array of objects that define
/// how htmx should handle responses matching specific status code patterns.
///
/// Supported only in HTMX 2.x.
[HtmlAttributeName("response-handling")]
- public IList? ResponseHandling
+ public IList? ResponseHandling
{
get => _config.ResponseHandling;
set => _config.ResponseHandling = value;
@@ -565,7 +565,7 @@ internal sealed class HtmxConfigData
public bool? IgnoreTitle { get; set; }
public bool? ScrollIntoViewOnBoost { get; set; }
public string? TriggerSpecsCache { get; set; }
- public IList? ResponseHandling { get; set; }
+ public IList? ResponseHandling { get; set; }
public bool? AllowNestedOobSwaps { get; set; }
public bool? HistoryRestoreAsHxRequest { get; set; }
public bool? ReportValidityOfForms { get; set; }
diff --git a/src/Ramstack.HtmxToolkit/TagHelpers/HtmxHeaderJsonSerializerContext.cs b/src/Ramstack.HtmxToolkit/TagHelpers/HtmxHeaderJsonSerializerContext.cs
index a2fc086..b780e5a 100644
--- a/src/Ramstack.HtmxToolkit/TagHelpers/HtmxHeaderJsonSerializerContext.cs
+++ b/src/Ramstack.HtmxToolkit/TagHelpers/HtmxHeaderJsonSerializerContext.cs
@@ -3,7 +3,5 @@
namespace Ramstack.HtmxToolkit.TagHelpers;
[JsonSerializable(typeof(IDictionary))]
-[JsonSourceGenerationOptions(
- WriteIndented = false,
- GenerationMode = JsonSourceGenerationMode.Serialization)]
+[JsonSourceGenerationOptions(WriteIndented = false, GenerationMode = JsonSourceGenerationMode.Serialization)]
internal partial class HtmxHeaderJsonSerializerContext : JsonSerializerContext;
diff --git a/src/Ramstack.HtmxToolkit/TagHelpers/HtmxHeaderTagHelper.cs b/src/Ramstack.HtmxToolkit/TagHelpers/HtmxHeaderTagHelper.cs
index 347b8b0..98f1f4a 100644
--- a/src/Ramstack.HtmxToolkit/TagHelpers/HtmxHeaderTagHelper.cs
+++ b/src/Ramstack.HtmxToolkit/TagHelpers/HtmxHeaderTagHelper.cs
@@ -25,16 +25,24 @@ public sealed class HtmxHeaderTagHelper : TagHelper
/// Gets or sets the hx-header attribute values.
///
[HtmlAttributeName(HeadersDictionaryName, DictionaryAttributePrefix = HeadersPrefix)]
- public IDictionary Headers { get; set; } = new Dictionary();
+ public IDictionary Headers
+ {
+ get => field ??= new Dictionary();
+ set;
+ }
///
public override Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{
- var headers = new HtmlString(
- JsonSerializer.Serialize(Headers, HtmxHeaderJsonSerializerContext.Default.IDictionaryStringString));
+ if (Headers is { Count: > 0 })
+ {
+ var info = HtmxHeaderJsonSerializerContext.Default.IDictionaryStringString;
+ var headers = new HtmlString(JsonSerializer.Serialize(Headers, info));
- output.Attributes.SetAttribute(
- new TagHelperAttribute("hx-headers", headers, HtmlAttributeValueStyle.SingleQuotes));
+ output.Attributes.SetAttribute(
+ new TagHelperAttribute("hx-headers", headers, HtmlAttributeValueStyle.SingleQuotes)
+ );
+ }
return Task.CompletedTask;
}
diff --git a/src/Ramstack.HtmxToolkit/TagHelpers/ResponseHandlingEntry.cs b/src/Ramstack.HtmxToolkit/TagHelpers/ResponseHandlingConfig.cs
similarity index 84%
rename from src/Ramstack.HtmxToolkit/TagHelpers/ResponseHandlingEntry.cs
rename to src/Ramstack.HtmxToolkit/TagHelpers/ResponseHandlingConfig.cs
index 193f16d..e7d2974 100644
--- a/src/Ramstack.HtmxToolkit/TagHelpers/ResponseHandlingEntry.cs
+++ b/src/Ramstack.HtmxToolkit/TagHelpers/ResponseHandlingConfig.cs
@@ -1,10 +1,9 @@
namespace Ramstack.HtmxToolkit.TagHelpers;
///
-/// Represents a single entry in the htmx response handling configuration.
-/// Each entry defines how htmx should handle responses matching a specific HTTP status code pattern.
+/// Represents the response handling configuration for responses matching a specific HTTP status code pattern.
///
-public sealed class ResponseHandlingEntry
+public sealed class ResponseHandlingConfig
{
///
/// Gets or sets a regular expression that will be tested against response status codes.
diff --git a/src/Ramstack.HtmxToolkit/TagHelpers/ResponseHandlingEntryTagHelper.cs b/src/Ramstack.HtmxToolkit/TagHelpers/ResponseHandlingTagHelper.cs
similarity index 61%
rename from src/Ramstack.HtmxToolkit/TagHelpers/ResponseHandlingEntryTagHelper.cs
rename to src/Ramstack.HtmxToolkit/TagHelpers/ResponseHandlingTagHelper.cs
index 29faa3d..e514b22 100644
--- a/src/Ramstack.HtmxToolkit/TagHelpers/ResponseHandlingEntryTagHelper.cs
+++ b/src/Ramstack.HtmxToolkit/TagHelpers/ResponseHandlingTagHelper.cs
@@ -9,9 +9,9 @@ namespace Ramstack.HtmxToolkit.TagHelpers;
///
/// Supported only in HTMX 2.x.
[HtmlTargetElement("response-handling", ParentTag = "htmx-config", TagStructure = TagStructure.WithoutEndTag)]
-public sealed class ResponseHandlingEntryTagHelper : TagHelper
+public sealed class ResponseHandlingTagHelper : TagHelper
{
- private readonly ResponseHandlingEntry _entry = new();
+ private readonly ResponseHandlingConfig _config = new();
///
/// Gets or sets a regular expression that will be tested against response status codes.
@@ -19,8 +19,8 @@ public sealed class ResponseHandlingEntryTagHelper : TagHelper
[HtmlAttributeName("code")]
public string? Code
{
- get => _entry.Code;
- set => _entry.Code = value;
+ get => _config.Code;
+ set => _config.Code = value;
}
///
@@ -29,8 +29,8 @@ public string? Code
[HtmlAttributeName("swap")]
public bool? Swap
{
- get => _entry.Swap;
- set => _entry.Swap = value;
+ get => _config.Swap;
+ set => _config.Swap = value;
}
///
@@ -39,8 +39,8 @@ public bool? Swap
[HtmlAttributeName("error")]
public bool? Error
{
- get => _entry.Error;
- set => _entry.Error = value;
+ get => _config.Error;
+ set => _config.Error = value;
}
///
@@ -49,8 +49,8 @@ public bool? Error
[HtmlAttributeName("ignore-title")]
public bool? IgnoreTitle
{
- get => _entry.IgnoreTitle;
- set => _entry.IgnoreTitle = value;
+ get => _config.IgnoreTitle;
+ set => _config.IgnoreTitle = value;
}
///
@@ -59,8 +59,8 @@ public bool? IgnoreTitle
[HtmlAttributeName("select")]
public string? Select
{
- get => _entry.Select;
- set => _entry.Select = value;
+ get => _config.Select;
+ set => _config.Select = value;
}
///
@@ -69,8 +69,8 @@ public string? Select
[HtmlAttributeName("target")]
public string? Target
{
- get => _entry.Target;
- set => _entry.Target = value;
+ get => _config.Target;
+ set => _config.Target = value;
}
///
@@ -79,22 +79,29 @@ public string? Target
[HtmlAttributeName("swap-override")]
public string? SwapOverride
{
- get => _entry.SwapOverride;
- set => _entry.SwapOverride = value;
+ get => _config.SwapOverride;
+ set => _config.SwapOverride = value;
}
///
- public override void Process(TagHelperContext context, TagHelperOutput output)
+ public override Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{
- if (context.Items.TryGetValue(typeof(HtmxConfigTagHelper), out var value))
+ if (context.Items.TryGetValue(typeof(HtmxConfigTagHelper), out var value) && value is HtmxConfigTagHelper parent)
{
- if (value is HtmxConfigTagHelper config)
- {
- config.ResponseHandling ??= new List();
- config.ResponseHandling.Add(_entry);
- }
+ parent.ResponseHandling ??= new List();
+ parent.ResponseHandling.Add(_config);
+
+ output.SuppressOutput();
+ return Task.CompletedTask;
}
- output.SuppressOutput();
+ Error_NotNested();
+ return Task.CompletedTask;
+ }
+
+ private static void Error_NotNested()
+ {
+ const string Message = "The '' tag helper can only be used inside the '' tag helper";
+ throw new InvalidOperationException(Message);
}
}
diff --git a/tests/Ramstack.HtmxToolkit.Tests/EnumHelperTests.cs b/tests/Ramstack.HtmxToolkit.Tests/EnumHelperTests.cs
new file mode 100644
index 0000000..19c5160
--- /dev/null
+++ b/tests/Ramstack.HtmxToolkit.Tests/EnumHelperTests.cs
@@ -0,0 +1,89 @@
+namespace Ramstack.HtmxToolkit.Tests;
+
+[TestFixture]
+public class EnumHelperTests
+{
+ [TestCase(HtmxSwap.InnerHtml, "innerHTML")]
+ [TestCase(HtmxSwap.OuterHtml, "outerHTML")]
+ [TestCase(HtmxSwap.BeforeBegin, "beforebegin")]
+ [TestCase(HtmxSwap.AfterBegin, "afterbegin")]
+ [TestCase(HtmxSwap.BeforeEnd, "beforeend")]
+ [TestCase(HtmxSwap.AfterEnd, "afterend")]
+ [TestCase(HtmxSwap.Delete, "delete")]
+ [TestCase(HtmxSwap.None, "none")]
+ public void GetSwapValue_ReturnsExpectedString(HtmxSwap value, string expected) =>
+ Assert.That(value.GetSwapValue(), Is.EqualTo(expected));
+
+ [Test]
+ public void GetSwapValue_ReturnsNull_ForNullValue()
+ {
+ HtmxSwap? value = null;
+ Assert.That(value.GetSwapValue(), Is.Null);
+ }
+
+ [TestCase("innerHTML", HtmxSwap.InnerHtml)]
+ [TestCase("outerHTML", HtmxSwap.OuterHtml)]
+ [TestCase("beforebegin", HtmxSwap.BeforeBegin)]
+ [TestCase("afterbegin", HtmxSwap.AfterBegin)]
+ [TestCase("beforeend", HtmxSwap.BeforeEnd)]
+ [TestCase("afterend", HtmxSwap.AfterEnd)]
+ [TestCase("delete", HtmxSwap.Delete)]
+ [TestCase("none", HtmxSwap.None)]
+ public void ParseHtmxSwap_ParsesValue(string expression, HtmxSwap expected) =>
+ Assert.That(EnumHelper.ParseHtmxSwap(expression), Is.EqualTo(expected));
+
+ [Test]
+ public void ParseHtmxSwap_IgnoresModifiers() =>
+ Assert.That(EnumHelper.ParseHtmxSwap("innerHTML show:#content"), Is.EqualTo(HtmxSwap.InnerHtml));
+
+ [Test]
+ public void ParseHtmxSwap_IsCaseInsensitive() =>
+ Assert.That(EnumHelper.ParseHtmxSwap("OuterHTML"), Is.EqualTo(HtmxSwap.OuterHtml));
+
+ [Test]
+ public void ParseHtmxSwap_ReturnsNull_ForUnknownValue() =>
+ Assert.That(EnumHelper.ParseHtmxSwap("bogus"), Is.Null);
+
+ [Test]
+ public void ParseHtmxSwap_ReturnsNull_ForNullExpression() =>
+ Assert.That(EnumHelper.ParseHtmxSwap(null), Is.Null);
+
+ [TestCase(HttpVerb.Get, "get")]
+ [TestCase(HttpVerb.Head, "head")]
+ [TestCase(HttpVerb.Post, "post")]
+ [TestCase(HttpVerb.Put, "put")]
+ [TestCase(HttpVerb.Delete, "delete")]
+ [TestCase(HttpVerb.Connect, "connect")]
+ [TestCase(HttpVerb.Options, "options")]
+ [TestCase(HttpVerb.Trace, "trace")]
+ [TestCase(HttpVerb.Patch, "patch")]
+ public void GetHttpVerbValue_ReturnsExpectedString(HttpVerb value, string expected) =>
+ Assert.That(value.GetHttpVerbValue(), Is.EqualTo(expected));
+
+ [TestCase(HtmxBinaryType.Blob, "blob")]
+ [TestCase(HtmxBinaryType.ArrayBuffer, "arraybuffer")]
+ public void GetWsBinaryTypeValue_ReturnsExpectedString(HtmxBinaryType value, string expected) =>
+ Assert.That(value.GetWsBinaryTypeValue(), Is.EqualTo(expected));
+
+ [TestCase(HtmxScrollBehavior.Auto, "auto")]
+ [TestCase(HtmxScrollBehavior.Smooth, "smooth")]
+ [TestCase(HtmxScrollBehavior.Instant, "instant")]
+ public void GetScrollBehaviorValue_ReturnsExpectedString(HtmxScrollBehavior value, string expected) =>
+ Assert.That(value.GetScrollBehaviorValue(), Is.EqualTo(expected));
+
+ [Test]
+ public void GetSwapValue_ReturnsNone_ForUndefinedValue() =>
+ Assert.That(((HtmxSwap)999).GetSwapValue(), Is.EqualTo("none"));
+
+ [Test]
+ public void GetHttpVerbValue_ReturnsPatch_ForUndefinedValue() =>
+ Assert.That(((HttpVerb)999).GetHttpVerbValue(), Is.EqualTo("patch"));
+
+ [Test]
+ public void GetScrollBehaviorValue_ReturnsInstant_ForUndefinedValue() =>
+ Assert.That(((HtmxScrollBehavior)999).GetScrollBehaviorValue(), Is.EqualTo("instant"));
+
+ [Test]
+ public void GetWsBinaryTypeValue_ReturnsArrayBuffer_ForUndefinedValue() =>
+ Assert.That(((HtmxBinaryType)999).GetWsBinaryTypeValue(), Is.EqualTo("arraybuffer"));
+}
diff --git a/tests/Ramstack.HtmxToolkit.Tests/HtmxConfigTagHelperTests.cs b/tests/Ramstack.HtmxToolkit.Tests/HtmxConfigTagHelperTests.cs
new file mode 100644
index 0000000..0d73752
--- /dev/null
+++ b/tests/Ramstack.HtmxToolkit.Tests/HtmxConfigTagHelperTests.cs
@@ -0,0 +1,215 @@
+using Microsoft.AspNetCore.Antiforgery;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc.Rendering;
+using Microsoft.AspNetCore.Razor.TagHelpers;
+
+namespace Ramstack.HtmxToolkit.Tests;
+
+[TestFixture]
+public class HtmxConfigTagHelperTests
+{
+ [Test]
+ public async Task ProcessAsync_RendersMetaTag()
+ {
+ var output = TestHelper.CreateTagHelperOutput();
+ var helper = new HtmxConfigTagHelper(new StubAntiforgery());
+
+ await helper.ProcessAsync(TestHelper.CreateTagHelperContext(), output);
+
+ Assert.That(output.TagName, Is.EqualTo("meta"));
+ Assert.That(output.TagMode, Is.EqualTo(TagMode.SelfClosing));
+ Assert.That(output.Attributes["name"]!.Value, Is.EqualTo("htmx-config"));
+ }
+
+ [Test]
+ public async Task ProcessAsync_RemovesHtmxConfigAttribute_FromMetaTag()
+ {
+ var attributes = new TagHelperAttributeList
+ {
+ new TagHelperAttribute("htmx-config")
+ };
+
+ var output = TestHelper.CreateTagHelperOutput("meta", attributes);
+ var helper = new HtmxConfigTagHelper(new StubAntiforgery());
+
+ await helper.ProcessAsync(TestHelper.CreateTagHelperContext("meta", attributes), output);
+
+ Assert.That(output.Attributes["htmx-config"], Is.Null);
+ }
+
+ [Test]
+ public async Task ProcessAsync_WithDefaults_SerializesEmptyObject()
+ {
+ var output = TestHelper.CreateTagHelperOutput();
+ var helper = new HtmxConfigTagHelper(new StubAntiforgery());
+
+ await helper.ProcessAsync(TestHelper.CreateTagHelperContext(), output);
+
+ Assert.That(GetContent(output), Is.EqualTo("{}"));
+ }
+
+ [Test]
+ public async Task ProcessAsync_SerializesConfiguredOptions()
+ {
+ var helper = new HtmxConfigTagHelper(new StubAntiforgery())
+ {
+ HistoryEnabled = false,
+ HistoryCacheSize = 42,
+ RefreshOnHistoryMiss = true,
+ DefaultSwapStyle = HtmxSwap.OuterHtml,
+ DefaultSwapDelay = 250,
+ DefaultSettleDelay = 300,
+ IncludeIndicatorStyles = false,
+ IndicatorClass = "my-indicator",
+ RequestClass = "my-request",
+ AddedClass = "my-added",
+ SwappingClass = "my-swapping",
+ SettlingClass = "my-settling",
+ AllowEval = false,
+ AllowScriptTags = false,
+ InlineScriptNonce = "nonce",
+ InlineStyleNonce = "style-nonce",
+ AttributesToSettle = ["class", "style"],
+ UseTemplateFragments = true,
+ WsReconnectDelay = "exponential",
+ WsBinaryType = HtmxBinaryType.ArrayBuffer,
+ DisableSelector = "[data-disable]",
+ WithCredentials = true,
+ DisableInheritance = true,
+ Timeout = 5000,
+ ScrollBehavior = HtmxScrollBehavior.Instant,
+ DefaultFocusScroll = true,
+ GetCacheBusterParam = true,
+ GlobalViewTransitions = true,
+ MethodsThatUseUrlParams = [HttpVerb.Get, HttpVerb.Delete],
+ SelfRequestsOnly = true,
+ IgnoreTitle = true,
+ ScrollIntoViewOnBoost = false,
+ TriggerSpecsCache = "{}",
+ AllowNestedOobSwaps = true,
+ HistoryRestoreAsHxRequest = false,
+ ReportValidityOfForms = true
+ };
+
+ var output = TestHelper.CreateTagHelperOutput();
+ await helper.ProcessAsync(TestHelper.CreateTagHelperContext(), output);
+
+ var json = JsonHelper.ParseJson(GetContent(output));
+
+ Assert.That(json["historyEnabled"].GetBoolean(), Is.False);
+ Assert.That(json["historyCacheSize"].GetInt32(), Is.EqualTo(42));
+ Assert.That(json["refreshOnHistoryMiss"].GetBoolean(), Is.True);
+ Assert.That(json["defaultSwapStyle"].GetString(), Is.EqualTo("outerHTML"));
+ Assert.That(json["defaultSwapDelay"].GetInt32(), Is.EqualTo(250));
+ Assert.That(json["defaultSettleDelay"].GetInt32(), Is.EqualTo(300));
+ Assert.That(json["includeIndicatorStyles"].GetBoolean(), Is.False);
+ Assert.That(json["indicatorClass"].GetString(), Is.EqualTo("my-indicator"));
+ Assert.That(json["requestClass"].GetString(), Is.EqualTo("my-request"));
+ Assert.That(json["addedClass"].GetString(), Is.EqualTo("my-added"));
+ Assert.That(json["swappingClass"].GetString(), Is.EqualTo("my-swapping"));
+ Assert.That(json["settlingClass"].GetString(), Is.EqualTo("my-settling"));
+ Assert.That(json["allowEval"].GetBoolean(), Is.False);
+ Assert.That(json["allowScriptTags"].GetBoolean(), Is.False);
+ Assert.That(json["inlineScriptNonce"].GetString(), Is.EqualTo("nonce"));
+ Assert.That(json["inlineStyleNonce"].GetString(), Is.EqualTo("style-nonce"));
+ Assert.That(json["attributesToSettle"].GetRawText(), Is.EqualTo("[\"class\",\"style\"]"));
+ Assert.That(json["useTemplateFragments"].GetBoolean(), Is.True);
+ Assert.That(json["wsReconnectDelay"].GetString(), Is.EqualTo("exponential"));
+ Assert.That(json["wsBinaryType"].GetString(), Is.EqualTo("arraybuffer"));
+ Assert.That(json["disableSelector"].GetString(), Is.EqualTo("[data-disable]"));
+ Assert.That(json["withCredentials"].GetBoolean(), Is.True);
+ Assert.That(json["disableInheritance"].GetBoolean(), Is.True);
+ Assert.That(json["timeout"].GetInt32(), Is.EqualTo(5000));
+ Assert.That(json["scrollBehavior"].GetString(), Is.EqualTo("instant"));
+ Assert.That(json["defaultFocusScroll"].GetBoolean(), Is.True);
+ Assert.That(json["getCacheBusterParam"].GetBoolean(), Is.True);
+ Assert.That(json["globalViewTransitions"].GetBoolean(), Is.True);
+ Assert.That(json["methodsThatUseUrlParams"].GetRawText(), Is.EqualTo("[\"get\",\"delete\"]"));
+ Assert.That(json["selfRequestsOnly"].GetBoolean(), Is.True);
+ Assert.That(json["ignoreTitle"].GetBoolean(), Is.True);
+ Assert.That(json["scrollIntoViewOnBoost"].GetBoolean(), Is.False);
+ Assert.That(json["triggerSpecsCache"].GetString(), Is.EqualTo("{}"));
+ Assert.That(json["allowNestedOobSwaps"].GetBoolean(), Is.True);
+ Assert.That(json["historyRestoreAsHxRequest"].GetBoolean(), Is.False);
+ Assert.That(json["reportValidityOfForms"].GetBoolean(), Is.True);
+ }
+
+ [Test]
+ public async Task ProcessAsync_IncludeAntiForgeryToken_SerializesTokens()
+ {
+ var antiforgery = new StubAntiforgery();
+ var httpContext = new DefaultHttpContext();
+ var helper = new HtmxConfigTagHelper(antiforgery)
+ {
+ IncludeAntiForgeryToken = true,
+ ViewContext = new ViewContext
+ {
+ HttpContext = httpContext
+ }
+ };
+
+ var output = TestHelper.CreateTagHelperOutput();
+ await helper.ProcessAsync(TestHelper.CreateTagHelperContext(), output);
+
+ Assert.That(antiforgery.StoredHttpContext, Is.SameAs(httpContext));
+
+ var json = JsonHelper.ParseJson(GetContent(output));
+ var antiForgery = json["antiForgery"];
+
+ Assert.That(antiForgery.GetProperty("requestToken").GetString(), Is.EqualTo("request-token"));
+ Assert.That(antiForgery.GetProperty("headerName").GetString(), Is.EqualTo("RequestVerificationToken"));
+ Assert.That(antiForgery.GetProperty("cookieToken").GetString(), Is.EqualTo("cookie-token"));
+ }
+
+ [Test]
+ public async Task ProcessAsync_ExecutesChildContent_CollectsResponseHandlingEntries()
+ {
+ var items = new Dictionary