diff --git a/README.md b/README.md
index f0dd73c..a3cb588 100644
--- a/README.md
+++ b/README.md
@@ -364,6 +364,15 @@ Response.Htmx(h => h
.StopPolling(ShouldStopPolling));
```
+`TriggerEvent` and `TriggerEvents` accept an optional `HtmxTriggerTiming` value.
+In HTMX 1.x and 2.x, the value selects `HX-Trigger`, `HX-Trigger-After-Swap`, or
+`HX-Trigger-After-Settle`. HTMX 4.x supports only `HX-Trigger`, so the toolkit
+emits events requested for any timing through that header rather than dropping
+them. These events run after the swap; in particular, the 1.x/2.x `Receive` and
+`AfterSettle` timings cannot be preserved. See
+[htmx pull request #3900](https://github.com/bigskysoftware/htmx/pull/3900) for
+the upstream timing change.
+
:bulb: The generic overload accepts an additional state parameter to avoid closure allocations:
```csharp
diff --git a/src/Ramstack.HtmxToolkit/HtmxResponse.cs b/src/Ramstack.HtmxToolkit/HtmxResponse.cs
index 8ea5c5d..fe965d3 100644
--- a/src/Ramstack.HtmxToolkit/HtmxResponse.cs
+++ b/src/Ramstack.HtmxToolkit/HtmxResponse.cs
@@ -205,6 +205,11 @@ public HtmxResponse Reselect(string value) =>
///
/// The current instance.
///
+ ///
+ /// In HTMX 4.x, every value is emitted through
+ /// HX-Trigger and runs after the swap.
+ /// See PR #3900.
+ ///
public HtmxResponse TriggerEvent(string eventName, HtmxTriggerTiming trigger = HtmxTriggerTiming.Receive) =>
TriggerEvent(eventName, "", trigger);
@@ -218,6 +223,11 @@ public HtmxResponse TriggerEvent(string eventName, HtmxTriggerTiming trigger = H
///
/// The current instance.
///
+ ///
+ /// In HTMX 4.x, every value is emitted through
+ /// HX-Trigger and runs after the swap.
+ /// See PR #3900.
+ ///
public HtmxResponse TriggerEvent(string eventName, object detail, HtmxTriggerTiming timing = HtmxTriggerTiming.Receive)
{
return TriggerEventImpl(this, eventName, detail, timing);
@@ -234,6 +244,11 @@ static HtmxResponse TriggerEventImpl(HtmxResponse response, string eventName, ob
///
/// The current instance.
///
+ ///
+ /// In HTMX 4.x, every value is emitted through
+ /// HX-Trigger and runs after the swap.
+ /// See PR #3900.
+ ///
public HtmxResponse TriggerEvents(IReadOnlyDictionary events, HtmxTriggerTiming timing = HtmxTriggerTiming.Receive) =>
AddEvents(this, events, timing);
diff --git a/src/Ramstack.HtmxToolkit/HtmxResponseHeaders.cs b/src/Ramstack.HtmxToolkit/HtmxResponseHeaders.cs
index 7ba7e24..f2ca589 100644
--- a/src/Ramstack.HtmxToolkit/HtmxResponseHeaders.cs
+++ b/src/Ramstack.HtmxToolkit/HtmxResponseHeaders.cs
@@ -120,8 +120,14 @@ public string Reselect
/// Gets or sets the client-side events to trigger through the HX-Trigger header.
///
///
- /// Event values are accumulated for the current response and serialized
- /// into the header immediately before the response starts.
+ ///
+ /// Event values are accumulated for the current response and serialized
+ /// into the header immediately before the response starts.
+ ///
+ ///
+ /// HTMX 1.x and 2.x trigger these events when the response is received,
+ /// whereas HTMX 4.x triggers them after the swap.
+ ///
///
[MaybeNull]
public IReadOnlyDictionary Trigger
@@ -139,7 +145,10 @@ public IReadOnlyDictionary Trigger
/// Event values are accumulated for the current response and serialized
/// into the header immediately before the response starts.
///
- /// This header is supported only in HTMX 1.x and 2.x.
+ ///
+ /// In HTMX 4.x, assigned events are accumulated in
+ /// and emitted through HX-Trigger after the swap.
+ ///
///
[MaybeNull]
public IReadOnlyDictionary TriggerAfterSwap
@@ -157,7 +166,11 @@ public IReadOnlyDictionary TriggerAfterSwap
/// Event values are accumulated for the current response and serialized
/// into the header immediately before the response starts.
///
- /// This header is supported only in HTMX 1.x and 2.x.
+ ///
+ /// In HTMX 4.x, assigned events are accumulated in
+ /// and emitted through HX-Trigger after the swap; the requested
+ /// after-settle timing cannot be preserved.
+ ///
///
[MaybeNull]
public IReadOnlyDictionary TriggerAfterSettle
diff --git a/src/Ramstack.HtmxToolkit/HtmxTriggerTiming.cs b/src/Ramstack.HtmxToolkit/HtmxTriggerTiming.cs
index d603282..df63f21 100644
--- a/src/Ramstack.HtmxToolkit/HtmxTriggerTiming.cs
+++ b/src/Ramstack.HtmxToolkit/HtmxTriggerTiming.cs
@@ -10,20 +10,36 @@ public enum HtmxTriggerTiming
///
///
/// HTMX 1.x and 2.x trigger these events when the response is received.
- /// HTMX 4.x triggers them after the swap completes.
+ ///
+ /// HTMX 4.x triggers them after the swap completes.
+ /// See PR #3900.
+ ///
///
Receive,
///
/// Maps to the HX-Trigger-After-Swap header, which triggers events after the swap step.
///
- /// Supported only in HTMX 1.x and 2.x.
+ ///
+ /// HTMX 1.x and 2.x emit these events through HX-Trigger-After-Swap.
+ ///
+ /// HTMX 4.x emits them through HX-Trigger, which also runs after the swap.
+ /// See PR #3900.
+ ///
+ ///
AfterSwap,
///
/// Maps to the HX-Trigger-After-Settle header, which triggers events
/// after the settle step.
///
- /// Supported only in HTMX 1.x and 2.x.
+ ///
+ /// HTMX 1.x and 2.x emit these events through HX-Trigger-After-Settle.
+ ///
+ /// HTMX 4.x emits them through HX-Trigger after the swap; the requested
+ /// after-settle timing cannot be preserved.
+ /// See PR #3900.
+ ///
+ ///
AfterSettle
}
diff --git a/src/Ramstack.HtmxToolkit/PendingEvents.cs b/src/Ramstack.HtmxToolkit/PendingEvents.cs
index a513e65..210c064 100644
--- a/src/Ramstack.HtmxToolkit/PendingEvents.cs
+++ b/src/Ramstack.HtmxToolkit/PendingEvents.cs
@@ -1,8 +1,10 @@
using System.Text.Json;
using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Options;
using Ramstack.HtmxToolkit.Collections;
+using Ramstack.HtmxToolkit.Configuration;
using Ramstack.HtmxToolkit.Serialization;
namespace Ramstack.HtmxToolkit;
@@ -16,6 +18,8 @@ internal sealed class PendingEvents
private const string ProxyEventName = "rs:events";
private readonly HttpResponse _response;
+ private readonly HtmxTargetVersion _version;
+
private SmallDictionary? _receive;
private SmallDictionary? _afterSwap;
private SmallDictionary? _afterSettle;
@@ -25,7 +29,7 @@ internal sealed class PendingEvents
///
/// The HTTP response to which the events belong.
private PendingEvents(HttpResponse response) =>
- _response = response;
+ (_response, _version) = (response, GetTargetVersion(response));
///
/// Adds the specified events to the pending set for .
@@ -36,6 +40,8 @@ private PendingEvents(HttpResponse response) =>
/// The event names and their associated details.
public void AddEvents(HtmxTriggerTiming timing, IReadOnlyDictionary events)
{
+ timing = NormalizeTiming(timing);
+
var current = timing switch
{
HtmxTriggerTiming.Receive => _receive ??= new SmallDictionary(StringComparer.Ordinal),
@@ -67,6 +73,8 @@ public void AddEvents(HtmxTriggerTiming timing, IReadOnlyDictionary
public IReadOnlyDictionary? GetEvents(HtmxTriggerTiming timing)
{
+ timing = NormalizeTiming(timing);
+
return timing switch
{
HtmxTriggerTiming.Receive => _receive,
@@ -82,6 +90,8 @@ public void AddEvents(HtmxTriggerTiming timing, IReadOnlyDictionaryThe replacement event names and their associated details.
public void SetEvents(HtmxTriggerTiming timing, IReadOnlyDictionary events)
{
+ timing = NormalizeTiming(timing);
+
var replacement = new SmallDictionary(events, StringComparer.Ordinal);
switch (timing)
{
@@ -156,4 +166,32 @@ private void SetHeader(string name, SmallDictionary? events)
if (events is not null)
_response.Headers[name] = JsonSerializer.Serialize(events, JsonOptions.CamelCase);
}
+
+ ///
+ /// Normalizes unsupported HTMX 4.x trigger timings to the primary trigger header.
+ ///
+ /// The requested event timing.
+ ///
+ /// The timing supported by the configured HTMX version.
+ ///
+ private HtmxTriggerTiming NormalizeTiming(HtmxTriggerTiming timing) =>
+ _version == HtmxTargetVersion.V4
+ ? HtmxTriggerTiming.Receive
+ : timing;
+
+ ///
+ /// Returns the configured HTMX target version, defaulting to HTMX 2.x
+ /// when toolkit services are unavailable.
+ ///
+ /// The response whose request services are inspected.
+ ///
+ /// The configured HTMX target version.
+ ///
+ private static HtmxTargetVersion GetTargetVersion(HttpResponse response)
+ {
+ var p = response.HttpContext.RequestServices;
+ var options = p.GetService(typeof(IOptions)) as IOptions;
+
+ return options?.Value.TargetVersion ?? HtmxTargetVersion.V2;
+ }
}
diff --git a/tests/Ramstack.HtmxToolkit.Tests/HtmxResponseHeadersTests.cs b/tests/Ramstack.HtmxToolkit.Tests/HtmxResponseHeadersTests.cs
index 6695a65..4564e1f 100644
--- a/tests/Ramstack.HtmxToolkit.Tests/HtmxResponseHeadersTests.cs
+++ b/tests/Ramstack.HtmxToolkit.Tests/HtmxResponseHeadersTests.cs
@@ -1,4 +1,4 @@
-using Microsoft.AspNetCore.Http;
+using Ramstack.HtmxToolkit.Configuration;
namespace Ramstack.HtmxToolkit.Tests;
@@ -8,7 +8,7 @@ public class HtmxResponseHeadersTests
[Test]
public void Properties_RoundTrips()
{
- var context = new DefaultHttpContext();
+ var context = TestHelper.CreateHttpContext();
var headers = context.Response.GetHtmxHeaders();
headers.Location = "/foo";
@@ -42,7 +42,7 @@ public void Properties_RoundTrips()
[Test]
public void Refresh_WhenTrue_SetsTrue()
{
- var context = new DefaultHttpContext();
+ var context = TestHelper.CreateHttpContext();
var headers = context.Response.GetHtmxHeaders();
headers.Refresh = true;
@@ -54,7 +54,7 @@ public void Refresh_WhenTrue_SetsTrue()
[Test]
public void Refresh_IsFalse_ByDefault()
{
- var context = new DefaultHttpContext();
+ var context = TestHelper.CreateHttpContext();
var headers = context.Response.GetHtmxHeaders();
Assert.That(headers.Refresh, Is.False);
@@ -63,7 +63,7 @@ public void Refresh_IsFalse_ByDefault()
[Test]
public void Reswap_IsNull_WhenHeaderAbsent()
{
- var context = new DefaultHttpContext();
+ var context = TestHelper.CreateHttpContext();
var headers = context.Response.GetHtmxHeaders();
Assert.That(headers.Reswap, Is.Null);
@@ -72,7 +72,7 @@ public void Reswap_IsNull_WhenHeaderAbsent()
[Test]
public void Reswap_IsNull_WhenHeaderUnknown()
{
- var context = new DefaultHttpContext();
+ var context = TestHelper.CreateHttpContext();
var headers = context.Response.GetHtmxHeaders();
context.Response.Headers[HtmxResponseHeaderNames.Reswap] = "bogus";
@@ -83,7 +83,7 @@ public void Reswap_IsNull_WhenHeaderUnknown()
[Test]
public void ReswapExpression_KeepsFullExpression_WhileReswapParsesOnlyStyle()
{
- var context = new DefaultHttpContext();
+ var context = TestHelper.CreateHttpContext();
var headers = context.Response.GetHtmxHeaders();
headers.ReswapExpression = "outerHTML show:top";
@@ -95,7 +95,7 @@ public void ReswapExpression_KeepsFullExpression_WhileReswapParsesOnlyStyle()
[Test]
public void SettingNull_DoesNotAddHeader()
{
- var context = new DefaultHttpContext();
+ var context = TestHelper.CreateHttpContext();
var headers = context.Response.GetHtmxHeaders();
headers.Location = null!;
@@ -106,11 +106,37 @@ public void SettingNull_DoesNotAddHeader()
[Test]
public void Trigger_IsNull_WhenNotSet()
{
- var context = new DefaultHttpContext();
+ var context = TestHelper.CreateHttpContext();
var headers = context.Response.GetHtmxHeaders();
Assert.That(headers.Trigger, Is.Null);
Assert.That(headers.TriggerAfterSwap, Is.Null);
Assert.That(headers.TriggerAfterSettle, Is.Null);
}
+
+ [Test]
+ public void TriggerTimingProperties_Htmx4_AliasReceiveTrigger()
+ {
+ var context = TestHelper.CreateHtmxRequestContext(HtmxTargetVersion.V4);
+ var headers = context.Response.GetHtmxHeaders();
+ var events = new Dictionary { ["swapped"] = true };
+
+ headers.TriggerAfterSwap = events;
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(headers.Trigger, Is.EqualTo(events));
+ Assert.That(headers.TriggerAfterSwap, Is.EqualTo(events));
+ Assert.That(headers.TriggerAfterSettle, Is.EqualTo(events));
+ });
+
+ PendingEvents.GetOrCreate(context.Response).Flush();
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(context.Response.Headers.ContainsKey(HtmxResponseHeaderNames.Trigger), Is.True);
+ Assert.That(context.Response.Headers.ContainsKey(HtmxResponseHeaderNames.TriggerAfterSwap), Is.False);
+ Assert.That(context.Response.Headers.ContainsKey(HtmxResponseHeaderNames.TriggerAfterSettle), Is.False);
+ });
+ }
}
diff --git a/tests/Ramstack.HtmxToolkit.Tests/HtmxResponseTests.cs b/tests/Ramstack.HtmxToolkit.Tests/HtmxResponseTests.cs
index 40860b6..8148ff6 100644
--- a/tests/Ramstack.HtmxToolkit.Tests/HtmxResponseTests.cs
+++ b/tests/Ramstack.HtmxToolkit.Tests/HtmxResponseTests.cs
@@ -1,3 +1,5 @@
+using Ramstack.HtmxToolkit.Configuration;
+
namespace Ramstack.HtmxToolkit.Tests;
[TestFixture]
@@ -260,4 +262,25 @@ public void TriggerEvents_SetsMultipleEvents()
Assert.That(events["a"], Is.EqualTo(1));
Assert.That(events["b"], Is.EqualTo(2));
}
+
+ [Test]
+ public void TriggerEvent_Htmx4_AddsEveryTimingToReceiveTrigger()
+ {
+ var context = TestHelper.CreateHtmxRequestContext(HtmxTargetVersion.V4);
+
+ context.Response.Htmx(r => r
+ .TriggerEvent("received")
+ .TriggerEvent("swapped", HtmxTriggerTiming.AfterSwap)
+ .TriggerEvent("settled", HtmxTriggerTiming.AfterSettle));
+
+ var headers = context.Response.GetHtmxHeaders();
+ var events = headers.Trigger!;
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(events.Keys, Is.EqualTo(["received", "swapped", "settled"]));
+ Assert.That(headers.TriggerAfterSwap, Is.SameAs(events));
+ Assert.That(headers.TriggerAfterSettle, Is.SameAs(events));
+ });
+ }
}
diff --git a/tests/Ramstack.HtmxToolkit.Tests/PendingEventsTests.cs b/tests/Ramstack.HtmxToolkit.Tests/PendingEventsTests.cs
index 46e1dae..8c5f069 100644
--- a/tests/Ramstack.HtmxToolkit.Tests/PendingEventsTests.cs
+++ b/tests/Ramstack.HtmxToolkit.Tests/PendingEventsTests.cs
@@ -1,4 +1,4 @@
-using Microsoft.AspNetCore.Http;
+using Ramstack.HtmxToolkit.Configuration;
namespace Ramstack.HtmxToolkit.Tests;
@@ -8,7 +8,7 @@ public class PendingEventsTests
[Test]
public void AddEvents_StoresDistinctKeys()
{
- var context = new DefaultHttpContext();
+ var context = TestHelper.CreateHttpContext();
var pending = PendingEvents.GetOrCreate(context.Response);
pending.AddEvents(
@@ -25,7 +25,7 @@ public void AddEvents_StoresDistinctKeys()
[Test]
public void AddEvents_AccumulatesDuplicateKeys_UnderProxy()
{
- var context = new DefaultHttpContext();
+ var context = TestHelper.CreateHttpContext();
var pending = PendingEvents.GetOrCreate(context.Response);
pending.AddEvents(
@@ -46,7 +46,7 @@ public void AddEvents_AccumulatesDuplicateKeys_UnderProxy()
[Test]
public void AddEvents_TracksTimingsIndependently()
{
- var context = new DefaultHttpContext();
+ var context = TestHelper.CreateHttpContext();
var pending = PendingEvents.GetOrCreate(context.Response);
pending.AddEvents(HtmxTriggerTiming.Receive, CreateDictionary(("r", 1)));
@@ -58,10 +58,89 @@ public void AddEvents_TracksTimingsIndependently()
Assert.That(pending.GetEvents(HtmxTriggerTiming.AfterSwap), Is.EqualTo(CreateDictionary(("s", 2))));
}
+ [TestCase(HtmxTargetVersion.V1)]
+ [TestCase(HtmxTargetVersion.V2)]
+ public void AddEvents_LegacyVersions_WriteEachTimingToItsOwnHeader(HtmxTargetVersion targetVersion)
+ {
+ var context = TestHelper.CreateHtmxRequestContext(targetVersion);
+ var pending = PendingEvents.GetOrCreate(context.Response);
+
+ pending.AddEvents(HtmxTriggerTiming.Receive, CreateDictionary(("received", 1)));
+ pending.AddEvents(HtmxTriggerTiming.AfterSwap, CreateDictionary(("swapped", 2)));
+ pending.AddEvents(HtmxTriggerTiming.AfterSettle, CreateDictionary(("settled", 3)));
+ pending.Flush();
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(
+ context.Response.Headers[HtmxResponseHeaderNames.Trigger].ToString(),
+ Is.EqualTo("{\"received\":1}"));
+
+ Assert.That(
+ context.Response.Headers[HtmxResponseHeaderNames.TriggerAfterSwap].ToString(),
+ Is.EqualTo("{\"swapped\":2}"));
+
+ Assert.That(
+ context.Response.Headers[HtmxResponseHeaderNames.TriggerAfterSettle].ToString(),
+ Is.EqualTo("{\"settled\":3}"));
+ });
+ }
+
+ [Test]
+ public void AddEvents_Htmx4_NormalizesAllTimingsToReceive()
+ {
+ var context = TestHelper.CreateHtmxRequestContext(HtmxTargetVersion.V4);
+ var pending = PendingEvents.GetOrCreate(context.Response);
+
+ pending.AddEvents(HtmxTriggerTiming.Receive, CreateDictionary(("received", 1)));
+ pending.AddEvents(HtmxTriggerTiming.AfterSwap, CreateDictionary(("swapped", 2)));
+ pending.AddEvents(HtmxTriggerTiming.AfterSettle, CreateDictionary(("settled", 3)));
+
+ var events = pending.GetEvents(HtmxTriggerTiming.Receive)!;
+ pending.Flush();
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(events.Keys, Is.EqualTo(["received", "swapped", "settled"]));
+ Assert.That(pending.GetEvents(HtmxTriggerTiming.AfterSwap), Is.SameAs(events));
+ Assert.That(pending.GetEvents(HtmxTriggerTiming.AfterSettle), Is.SameAs(events));
+
+ Assert.That(
+ context.Response.Headers[HtmxResponseHeaderNames.Trigger].ToString(),
+ Is.EqualTo("{\"received\":1,\"swapped\":2,\"settled\":3}"));
+
+ Assert.That(context.Response.Headers.ContainsKey(HtmxResponseHeaderNames.TriggerAfterSwap), Is.False);
+ Assert.That(context.Response.Headers.ContainsKey(HtmxResponseHeaderNames.TriggerAfterSettle), Is.False);
+ });
+ }
+
+ [Test]
+ public void AddEvents_Htmx4_PreservesDuplicatesAcrossRequestedTimings()
+ {
+ var context = TestHelper.CreateHtmxRequestContext(HtmxTargetVersion.V4);
+ var pending = PendingEvents.GetOrCreate(context.Response);
+
+ pending.AddEvents(HtmxTriggerTiming.Receive, CreateDictionary(("message", "first")));
+ pending.AddEvents(HtmxTriggerTiming.AfterSwap, CreateDictionary(("message", "second")));
+ pending.AddEvents(HtmxTriggerTiming.AfterSettle, CreateDictionary(("message", "third")));
+
+ var events = pending.GetEvents(HtmxTriggerTiming.Receive)!;
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(events["message"], Is.EqualTo("first"));
+ Assert.That(events["rs:events"], Is.EqualTo(new[]
+ {
+ KeyValuePair.Create("message", "second"),
+ KeyValuePair.Create("message", "third")
+ }));
+ });
+ }
+
[Test]
public void GetEvents_ReturnsNull_WhenNothingRegistered()
{
- var context = new DefaultHttpContext();
+ var context = TestHelper.CreateHttpContext();
var pending = PendingEvents.GetOrCreate(context.Response);
Assert.That(pending.GetEvents(HtmxTriggerTiming.Receive), Is.Null);
@@ -72,7 +151,7 @@ public void GetEvents_ReturnsNull_WhenNothingRegistered()
[Test]
public void SetEvents_ReplacesExisting()
{
- var context = new DefaultHttpContext();
+ var context = TestHelper.CreateHttpContext();
var pending = PendingEvents.GetOrCreate(context.Response);
pending.AddEvents(HtmxTriggerTiming.Receive, CreateDictionary(("old", 1)));
@@ -86,7 +165,7 @@ public void SetEvents_ReplacesExisting()
[Test]
public void Flush_WritesCamelCaseJsonToHeaders()
{
- var context = new DefaultHttpContext();
+ var context = TestHelper.CreateHttpContext();
var pending = PendingEvents.GetOrCreate(context.Response);
pending.AddEvents(
@@ -103,7 +182,7 @@ public void Flush_WritesCamelCaseJsonToHeaders()
[Test]
public void Flush_WritesOnlyRegisteredTimings()
{
- var context = new DefaultHttpContext();
+ var context = TestHelper.CreateHttpContext();
var pending = PendingEvents.GetOrCreate(context.Response);
pending.AddEvents(
@@ -120,7 +199,7 @@ public void Flush_WritesOnlyRegisteredTimings()
[Test]
public void Flush_SerializesNullEventDetail_AsNull()
{
- var context = new DefaultHttpContext();
+ var context = TestHelper.CreateHttpContext();
var pending = PendingEvents.GetOrCreate(context.Response);
pending.AddEvents(HtmxTriggerTiming.Receive, CreateDictionary(("e", null!)));
@@ -133,7 +212,7 @@ public void Flush_SerializesNullEventDetail_AsNull()
[Test]
public void GetOrCreate_ReturnsSameInstance()
{
- var context = new DefaultHttpContext();
+ var context = TestHelper.CreateHttpContext();
var pending = PendingEvents.GetOrCreate(context.Response);
Assert.That(pending, Is.SameAs(PendingEvents.GetOrCreate(context.Response)));
@@ -143,7 +222,7 @@ public void GetOrCreate_ReturnsSameInstance()
[Test]
public void GetOrCreate_RegistersInHttpContextItems()
{
- var context = new DefaultHttpContext();
+ var context = TestHelper.CreateHttpContext();
var pending = PendingEvents.GetOrCreate(context.Response);
Assert.That(context.Items[typeof(PendingEvents)], Is.SameAs(pending));
diff --git a/tests/Ramstack.HtmxToolkit.Tests/TestHelper.cs b/tests/Ramstack.HtmxToolkit.Tests/TestHelper.cs
index ada0cee..5f3fe0c 100644
--- a/tests/Ramstack.HtmxToolkit.Tests/TestHelper.cs
+++ b/tests/Ramstack.HtmxToolkit.Tests/TestHelper.cs
@@ -1,5 +1,9 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Razor.TagHelpers;
+using Microsoft.Extensions.DependencyInjection;
+
+using Ramstack.HtmxToolkit.Configuration;
+using Ramstack.HtmxToolkit.Hosting;
namespace Ramstack.HtmxToolkit.Tests;
@@ -17,7 +21,7 @@ internal static class TestHelper
///
public static HttpContext CreateHttpContext(params (string Name, string Value)[] headers)
{
- var context = new DefaultHttpContext();
+ var context = CreateContext(HtmxTargetVersion.V2);
foreach (var (name, value) in headers)
context.Request.Headers[name] = value;
@@ -35,7 +39,28 @@ public static HttpContext CreateHttpContext(params (string Name, string Value)[]
///
public static HttpContext CreateHtmxRequestContext(bool boosted = false)
{
- var context = new DefaultHttpContext();
+ var context = CreateContext(HtmxTargetVersion.V2);
+ context.Request.Headers[HtmxRequestHeaderNames.Request] = "true";
+
+ if (boosted)
+ context.Request.Headers[HtmxRequestHeaderNames.Boosted] = "true";
+
+ return context;
+ }
+
+ ///
+ /// Creates a whose request is an htmx request
+ /// and whose services target the specified HTMX major version.
+ ///
+ /// The HTMX major version to configure.
+ /// to mark the request as boosted;
+ /// otherwise, .
+ ///
+ /// The configured .
+ ///
+ public static HttpContext CreateHtmxRequestContext(HtmxTargetVersion version, bool boosted = false)
+ {
+ var context = CreateContext(version);
context.Request.Headers[HtmxRequestHeaderNames.Request] = "true";
if (boosted)
@@ -44,6 +69,42 @@ public static HttpContext CreateHtmxRequestContext(bool boosted = false)
return context;
}
+ ///
+ /// Creates an HTTP context whose request services contain the toolkit
+ /// configuration for the specified HTMX major version.
+ ///
+ /// The HTMX major version to configure.
+ ///
+ /// The configured HTTP context.
+ ///
+ private static HttpContext CreateContext(HtmxTargetVersion version)
+ {
+ var services = new ServiceCollection();
+
+ services.AddHtmxToolkit(options =>
+ {
+ switch (version)
+ {
+ case HtmxTargetVersion.V1:
+ options.UseHtmxV1();
+ break;
+ case HtmxTargetVersion.V2:
+ options.UseHtmxV2();
+ break;
+ case HtmxTargetVersion.V4:
+ options.UseHtmxV4();
+ break;
+ default:
+ throw new ArgumentOutOfRangeException(nameof(version));
+ }
+ });
+
+ return new DefaultHttpContext
+ {
+ RequestServices = services.BuildServiceProvider()
+ };
+ }
+
///
/// Creates a with the given tag name, attributes and items.
///