diff --git a/src/Ramstack.HtmxToolkit/AjaxContext.cs b/src/Ramstack.HtmxToolkit/AjaxContext.cs deleted file mode 100644 index 3b5a166..0000000 --- a/src/Ramstack.HtmxToolkit/AjaxContext.cs +++ /dev/null @@ -1,53 +0,0 @@ -namespace Ramstack.HtmxToolkit; - -/// -/// Represents the context for an AJAX request. -/// -public sealed class AjaxContext -{ - /// - /// Gets or sets the path used for the AJAX request. - /// - internal string? Path { get; set; } - - /// - /// Gets or sets the source element that initiated the request. - /// - public string? Source { get; set; } - - /// - /// Gets or sets the event that triggered the request. - /// - public string? Event { get; set; } - - /// - /// Gets or sets the name of the client-side callback function that will handle the response HTML. - /// - public string? Handler { get; set; } - - /// - /// Gets or sets the target element into which the response will be swapped. - /// - public string? Target { get; set; } - - /// - /// Gets or sets how the response will be swapped relative to the target element. - /// - public HtmxSwap? Swap { get; set; } - - /// - /// Gets or sets the values to submit with the request. - /// - public object? Values { get; set; } - - /// - /// Gets or sets the headers to include with the request. - /// Header values must be strings; complex data should be passed as a pre-serialized JSON string. - /// - public IDictionary? Headers { get; set; } - - /// - /// Gets or sets a selector used to filter the content to swap from the response. - /// - public string? Select { get; set; } -} diff --git a/src/Ramstack.HtmxToolkit/AjaxContextWrapper.cs b/src/Ramstack.HtmxToolkit/AjaxContextWrapper.cs deleted file mode 100644 index ea72b15..0000000 --- a/src/Ramstack.HtmxToolkit/AjaxContextWrapper.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System.Text.Json.Serialization; - -using Ramstack.HtmxToolkit.Internal; - -namespace Ramstack.HtmxToolkit; - -/// -/// Represents a wrapper for the class, used for serialization. -/// -internal readonly struct AjaxContextWrapper -{ - private readonly AjaxContext _context; - - [JsonPropertyName("path")] public string? Path => _context.Path; - [JsonPropertyName("source")] public string? Source => _context.Source; - [JsonPropertyName("event")] public string? Event => _context.Event; - [JsonPropertyName("handler")] public string? Handler => _context.Handler; - [JsonPropertyName("target")] public string? Target => _context.Target; - [JsonPropertyName("swap")] public string? Swap => _context.Swap.GetSwapValue(); - [JsonPropertyName("values")] public object? Values => _context.Values; - [JsonPropertyName("headers")] public IDictionary? Headers => _context.Headers; - [JsonPropertyName("select")] public string? Select => _context.Select; - - /// - /// Initializes a new instance of the . - /// - /// The path of the AJAX request. - /// The . - public AjaxContextWrapper(string path, AjaxContext context) - { - context.Path = path; - _context = context; - } -} diff --git a/src/Ramstack.HtmxToolkit/HtmxFieldValues.cs b/src/Ramstack.HtmxToolkit/HtmxFieldValues.cs new file mode 100644 index 0000000..6e908c2 --- /dev/null +++ b/src/Ramstack.HtmxToolkit/HtmxFieldValues.cs @@ -0,0 +1,226 @@ +using System.Collections; +using System.Runtime.CompilerServices; +using System.Text.Json.Serialization; + +namespace Ramstack.HtmxToolkit; + +/// +/// Represents one or more values for a form field submitted with an HTMX request. +/// +/// +/// A single value can be assigned from a string. Multiple values can be specified with a collection expression. +/// +[JsonConverter(typeof(HtmxFieldValuesJsonConverter))] +[CollectionBuilder(typeof(HtmxFieldValues), nameof(Create))] +public readonly struct HtmxFieldValues : IReadOnlyList +{ + private readonly object? _values; + + /// + public int Count + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + var values = _values; + if (values is null) + return 0; + + if (values is string) + return 1; + + return Unsafe.As(values).Length; + } + } + + /// + public string this[int index] + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + var values = _values; + if (values is string s) + { + if (index == 0) + return s; + } + else if (values is not null) + { + return Unsafe.As(values)[index]; + } + + return OutOfBounds(); + } + } + + /// + /// Gets the underlying value representation. + /// + internal object? Values => _values; + + /// + /// Initializes a new instance of the structure with a single value. + /// + /// The value to store, or to represent no values. + public HtmxFieldValues(string? value) => + _values = value; + + /// + /// Initializes a new instance of the structure with the specified values. + /// + /// The values to store, or to represent no values. + /// + /// The specified array is stored directly and is not copied. + /// + public HtmxFieldValues(string[]? values) => + _values = values; + + /// + /// Creates an from the specified values. + /// + /// The values to include. + /// + /// An containing the specified values. + /// + public static HtmxFieldValues Create(ReadOnlySpan values) + { + return values.Length switch + { + 0 => new HtmxFieldValues([]), + 1 => new HtmxFieldValues(values[0]), + _ => new HtmxFieldValues(CreateArray(values)) + }; + } + + /// + /// Converts a string to an . + /// + /// The value to convert, or to represent no values. + public static implicit operator HtmxFieldValues(string? value) => + new(value); + + /// + /// Converts an array of strings to an . + /// + /// The values to convert, or to represent no values. + public static implicit operator HtmxFieldValues(string[]? values) => + new(values); + + /// + /// Returns an enumerator that iterates through the values. + /// + /// + /// An enumerator for the values. + /// + public Enumerator GetEnumerator() => + new(this); + + #region IEnumerable: explicit interface implementations + + /// + IEnumerator IEnumerable.GetEnumerator() => + GetEnumerator(); + + /// + IEnumerator IEnumerable.GetEnumerator() => + GetEnumerator(); + + #endregion + + [MethodImpl(MethodImplOptions.NoInlining)] + private static string OutOfBounds() => + Array.Empty()[0]; + + /// + /// Copies the specified read-only span to a new array. + /// + /// The values to copy. + /// + /// A new array containing the specified values. + /// + /// + /// The JIT compiler inlines the implementation of + /// into its caller, producing a disproportionately large amount of native code at the call site. + /// This non-inlined wrapper keeps that implementation out of . + /// + [MethodImpl(MethodImplOptions.NoInlining)] + private static string[] CreateArray(ReadOnlySpan s) => + s.ToArray(); + + #region Inner type: Enumerator + + /// + /// Enumerates the strings represented by an . + /// + public struct Enumerator : IEnumerator + { + private readonly string[]? _values; + private string? _current; + private int _index; + + /// + public readonly string Current => _current!; + + /// + /// Initializes a new enumerator for the specified . + /// + /// The value whose strings to enumerate. + public Enumerator(HtmxFieldValues value) + { + if (value.Values is string s) + { + (_values, _current) = (null, s); + } + else + { + (_values, _current) = (Unsafe.As(value.Values), null); + } + + _index = 0; + } + + /// + public bool MoveNext() + { + var index = _index; + if (index < 0) + return false; + + var values = _values; + if (values is not null) + { + if ((uint)index < (uint)values.Length) + { + _index = index + 1; + _current = values[index]; + return true; + } + + _index = -1; + return false; + } + + _index = -1; + return _current is not null; + } + + /// + public readonly void Dispose() + { + } + + #region IEnumerator: explicit interface implementations + + /// + readonly object IEnumerator.Current => Current; + + /// + void IEnumerator.Reset() => + throw new NotSupportedException(); + + #endregion + } + + #endregion +} diff --git a/src/Ramstack.HtmxToolkit/HtmxFieldValuesJsonConverter.cs b/src/Ramstack.HtmxToolkit/HtmxFieldValuesJsonConverter.cs new file mode 100644 index 0000000..98cd0d1 --- /dev/null +++ b/src/Ramstack.HtmxToolkit/HtmxFieldValuesJsonConverter.cs @@ -0,0 +1,37 @@ +using System.Runtime.CompilerServices; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Ramstack.HtmxToolkit; + +/// +/// Represents a JSON converter for . +/// +internal sealed class HtmxFieldValuesJsonConverter : JsonConverter +{ + /// + public override HtmxFieldValues Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => + throw new NotSupportedException(); + + /// + public override void Write(Utf8JsonWriter writer, HtmxFieldValues value, JsonSerializerOptions options) + { + var values = value.Values; + switch (values) + { + case string s: + writer.WriteStringValue(s); + break; + + default: + writer.WriteStartArray(); + + if (values is not null) + foreach (var s in Unsafe.As(values)) + writer.WriteStringValue(s); + + writer.WriteEndArray(); + break; + } + } +} diff --git a/src/Ramstack.HtmxToolkit/HtmxHistoryMode.cs b/src/Ramstack.HtmxToolkit/HtmxHistoryMode.cs index 96c07fa..c5eec00 100644 --- a/src/Ramstack.HtmxToolkit/HtmxHistoryMode.cs +++ b/src/Ramstack.HtmxToolkit/HtmxHistoryMode.cs @@ -1,5 +1,3 @@ -using System.Text.Json.Serialization; - namespace Ramstack.HtmxToolkit; /// diff --git a/src/Ramstack.HtmxToolkit/HtmxLocationOptions.cs b/src/Ramstack.HtmxToolkit/HtmxLocationOptions.cs new file mode 100644 index 0000000..7d96aad --- /dev/null +++ b/src/Ramstack.HtmxToolkit/HtmxLocationOptions.cs @@ -0,0 +1,73 @@ +using System.Text.Json.Serialization; + +namespace Ramstack.HtmxToolkit; + +/// +/// Represents the options for an HX-Location request. +/// +public sealed class HtmxLocationOptions +{ + /// + /// Gets or sets the path used for the AJAX request. + /// + /// Supported in HTMX 1.9.x, HTMX 2.x, and HTMX 4.x. + public string? Path { get; internal set; } + + /// + /// Gets or sets the source element that initiated the request. + /// + /// Supported in HTMX 1.9.x, HTMX 2.x, and HTMX 4.x. + public string? Source { get; set; } + + /// + /// Gets or sets the target element into which the response will be swapped. + /// + /// Supported in HTMX 1.9.x, HTMX 2.x, and HTMX 4.x. + public string? Target { get; set; } + + /// + /// 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. + [JsonConverter(typeof(HtmxSwapJsonConverter))] + public HtmxSwap? Swap { get; set; } + + /// + /// Gets or sets the form field values to submit with the request. + /// + /// Supported in HTMX 1.9.x, HTMX 2.x, and HTMX 4.x. + public IDictionary? Values { get; set; } + + /// + /// Gets or sets the headers to include with the request. + /// Header values must be strings; complex data should be passed as a pre-serialized JSON string. + /// + /// Supported in HTMX 1.9.x, HTMX 2.x, and HTMX 4.x. + public IDictionary? Headers { get; set; } + + /// + /// Gets or sets a selector used to filter the content to swap from the response. + /// + /// Supported in HTMX 1.9.x, HTMX 2.x, and HTMX 4.x. + public string? Select { get; set; } + + /// + /// Gets or sets a selector used to select content for out-of-band swaps from the response. + /// + /// Supported in HTMX 2.x and HTMX 4.x. + [JsonPropertyName("selectOOB")] + public string? SelectOob { get; set; } + + /// + /// Gets or sets the path to push into the browser history. + /// Set to false to prevent the URL from being pushed. + /// + /// Supported in HTMX 2.x and HTMX 4.x. + public string? Push { get; set; } + + /// + /// Gets or sets the path that replaces the current URL in the browser history. + /// + /// Supported in HTMX 2.x and HTMX 4.x. + public string? Replace { get; set; } +} diff --git a/src/Ramstack.HtmxToolkit/HtmxLocationOptionsJsonSerializerContext.cs b/src/Ramstack.HtmxToolkit/HtmxLocationOptionsJsonSerializerContext.cs new file mode 100644 index 0000000..90e5059 --- /dev/null +++ b/src/Ramstack.HtmxToolkit/HtmxLocationOptionsJsonSerializerContext.cs @@ -0,0 +1,26 @@ +using System.Text.Json.Serialization; + +using Ramstack.HtmxToolkit.Internal; + +namespace Ramstack.HtmxToolkit; + +/// +/// Provides source-generated JSON serialization metadata for HX-Location options. +/// +[JsonSourceGenerationOptions( + WriteIndented = false, + PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + GenerationMode = JsonSourceGenerationMode.Default)] +[JsonSerializable(typeof(HtmxLocationOptions))] +internal partial class HtmxLocationOptionsJsonSerializerContext : JsonSerializerContext +{ + /// + /// Initializes the default serializer context with HTML-safe Unicode encoding. + /// + static HtmxLocationOptionsJsonSerializerContext() + { + JsonOptions.ConfigureHtmlSafeUnicode(s_defaultOptions); + s_defaultContext = new HtmxLocationOptionsJsonSerializerContext(s_defaultOptions); + } +} diff --git a/src/Ramstack.HtmxToolkit/HtmxResponse.cs b/src/Ramstack.HtmxToolkit/HtmxResponse.cs index 83cdaa5..dfa1c8d 100644 --- a/src/Ramstack.HtmxToolkit/HtmxResponse.cs +++ b/src/Ramstack.HtmxToolkit/HtmxResponse.cs @@ -2,7 +2,6 @@ using System.Text.Json; using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Primitives; using Ramstack.HtmxToolkit.Internal; @@ -51,17 +50,19 @@ public HtmxResponse Location(string value) => /// Sets the HX-Location header to a client-side redirect that does not do a full page reload. /// /// The path of the request. - /// The AJAX context of the request. + /// The options for the HX-Location request. /// /// The current instance. /// - public HtmxResponse Location(string path, AjaxContext context) + public HtmxResponse Location(string path, HtmxLocationOptions options) { - return LocationImpl(this, path, context); + return LocationImpl(this, path, options); - static HtmxResponse LocationImpl(HtmxResponse response, string path, AjaxContext context) + static HtmxResponse LocationImpl(HtmxResponse response, string path, HtmxLocationOptions options) { - var value = JsonSerializer.Serialize(new AjaxContextWrapper(path, context), JsonOptions.PreserveKeyCase); + options.Path = path; + + var value = JsonSerializer.Serialize(options, HtmxLocationOptionsJsonSerializerContext.Default.HtmxLocationOptions); return SetHeader(response, HtmxResponseHeaderNames.Location, value); } } diff --git a/src/Ramstack.HtmxToolkit/Internal/JsonOptions.cs b/src/Ramstack.HtmxToolkit/Internal/JsonOptions.cs index 4b8e29f..c0fd200 100644 --- a/src/Ramstack.HtmxToolkit/Internal/JsonOptions.cs +++ b/src/Ramstack.HtmxToolkit/Internal/JsonOptions.cs @@ -34,14 +34,4 @@ public static void ConfigureHtmlSafeUnicode(JsonSerializerOptions options) => DictionaryKeyPolicy = JsonNamingPolicy.CamelCase, WriteIndented = false }; - - /// - /// JSON serializer options that preserve original property and key casing - /// while ignoring properties with values. - /// - public static readonly JsonSerializerOptions PreserveKeyCase = new() - { - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, - WriteIndented = false - }; } diff --git a/src/Ramstack.HtmxToolkit/Properties/CollectionBuilderAttribute.cs b/src/Ramstack.HtmxToolkit/Properties/CollectionBuilderAttribute.cs new file mode 100644 index 0000000..444806b --- /dev/null +++ b/src/Ramstack.HtmxToolkit/Properties/CollectionBuilderAttribute.cs @@ -0,0 +1,31 @@ +#if !NET8_0_OR_GREATER +namespace System.Runtime.CompilerServices; + +/// +/// Indicates the method used to build a collection from a collection expression. +/// +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Interface, Inherited = false)] +internal sealed class CollectionBuilderAttribute : Attribute +{ + /// + /// Initializes a new instance of the class. + /// + /// The type containing the builder method. + /// The name of the builder method. + public CollectionBuilderAttribute(Type builderType, string methodName) + { + BuilderType = builderType; + MethodName = methodName; + } + + /// + /// Gets the type containing the builder method. + /// + public Type BuilderType { get; } + + /// + /// Gets the name of the builder method. + /// + public string MethodName { get; } +} +#endif diff --git a/tests/Ramstack.HtmxToolkit.Tests/HtmxFieldValuesTests.cs b/tests/Ramstack.HtmxToolkit.Tests/HtmxFieldValuesTests.cs new file mode 100644 index 0000000..caee480 --- /dev/null +++ b/tests/Ramstack.HtmxToolkit.Tests/HtmxFieldValuesTests.cs @@ -0,0 +1,266 @@ +using System.Collections; +using System.Text.Json; + +namespace Ramstack.HtmxToolkit.Tests; + +[TestFixture] +public class HtmxFieldValuesTests +{ + [Test] + public void DefaultValue_IsEmpty() + { + var value = default(HtmxFieldValues); + + Assert.Multiple(() => + { + Assert.That(value.Count, Is.Zero); + Assert.That(value.Values, Is.Null); + Assert.That(value, Is.Empty); + Assert.That(value, Is.EquivalentTo(Array.Empty())); + }); + } + + [Test] + public void Enumeration_DefaultValue_ForeachCompletesWithoutItems() + { + var values = default(HtmxFieldValues); + var list = new List(); + + Assert.DoesNotThrow(() => + { + foreach (var item in values) + list.Add(item); + }); + + Assert.That(list, Is.Empty); + } + + [Test] + public void Constructor_SingleValue_UsesStringRepresentation() + { + var values = new HtmxFieldValues("value"); + Assert.That(values, Is.EqualTo(["value"])); + } + + [Test] + public void Constructor_Array_StoresArrayWithoutCopying() + { + var source = new[] { "first", "second" }; + var values = new HtmxFieldValues(source); + + source[1] = "changed"; + + Assert.Multiple(() => + { + Assert.That(values.Count, Is.EqualTo(2)); + Assert.That(values.Values, Is.SameAs(source)); + Assert.That(values[1], Is.EqualTo("changed")); + Assert.That(values, Is.EqualTo(source)); + }); + } + + [Test] + public void Constructor_EmptyArray_PreservesArrayRepresentation() + { + var source = new string [0]; + var values = new HtmxFieldValues(source); + + Assert.Multiple(() => + { + Assert.That(values.Count, Is.Zero); + Assert.That(values.Values, Is.SameAs(source)); + Assert.That(values, Is.Empty); + }); + } + + [Test] + public void Constructor_NullArray_IsEmpty() + { + var single = new HtmxFieldValues((string?)null); + var multiple = new HtmxFieldValues((string[]?)null); + + Assert.Multiple(() => + { + Assert.That(single.Count, Is.Zero); + Assert.That(multiple.Count, Is.Zero); + + Assert.That(single.Values, Is.Null); + Assert.That(multiple.Values, Is.Null); + + Assert.That(single, Is.Empty); + Assert.That(multiple, Is.Empty); + }); + } + + [Test] + public void Create_EmptySpan_ReturnsEmptyArrayRepresentation() + { + var values = HtmxFieldValues.Create(ReadOnlySpan.Empty); + + Assert.Multiple(() => + { + Assert.That(values.Count, Is.Zero); + Assert.That(values.Values, Is.TypeOf()); + Assert.That(values.Values, Is.Empty); + }); + } + + [Test] + public void Create_SingleValue_UsesStringRepresentation() + { + var source = new[] { "single" }; + var values = HtmxFieldValues.Create(source); + + Assert.Multiple(() => + { + Assert.That(values.Count, Is.EqualTo(1)); + Assert.That(values.Values, Is.TypeOf()); + Assert.That(values[0], Is.EqualTo("single")); + Assert.That(values, Is.EqualTo(["single"])); + }); + } + + [Test] + public void Create_MultipleValues_CopiesTheSpan() + { + var source = new[] { "before", "first", "second", "after" }; + var values = HtmxFieldValues.Create(source.AsSpan()); + source[1] = "changed"; + + Assert.Multiple(() => + { + Assert.That(values.Count, Is.EqualTo(4)); + Assert.That(values.Values, Is.TypeOf()); + Assert.That(values.Values, Is.Not.SameAs(source)); + Assert.That(values, Is.EqualTo(["before", "first", "second", "after"])); + }); + } + + [Test] + public void CollectionExpressions_SelectCompactRepresentations() + { + HtmxFieldValues empty = []; + HtmxFieldValues single = ["one"]; + HtmxFieldValues multiple = ["one", "two"]; + + Assert.Multiple(() => + { + Assert.That(empty.Values, Is.TypeOf()); + Assert.That(empty, Is.Empty); + Assert.That(single.Values, Is.TypeOf()); + Assert.That(single, Is.EqualTo(["one"])); + Assert.That(multiple.Values, Is.TypeOf()); + Assert.That(multiple, Is.EqualTo(["one", "two"])); + }); + } + + [Test] + public void ImplicitConversions_PreserveSourceRepresentations() + { + var array = new[] { "one", "two" }; + + HtmxFieldValues single = "value"; + HtmxFieldValues multiple = array; + + Assert.Multiple(() => + { + Assert.That(single.Values, Is.EqualTo("value")); + Assert.That(single, Is.EqualTo(["value"])); + Assert.That(multiple.Values, Is.SameAs(array)); + Assert.That(multiple, Is.EqualTo(array)); + }); + } + + [Test] + public void ImplicitConversions_NullSources_AreEmpty() + { + HtmxFieldValues single = (string?)null; + HtmxFieldValues multiple = (string[]?)null; + + Assert.Multiple(() => + { + Assert.That(single.Count, Is.Zero); + Assert.That(multiple.Count, Is.Zero); + + Assert.That(single.Values, Is.Null); + Assert.That(multiple.Values, Is.Null); + + Assert.That(single, Is.Empty); + Assert.That(multiple, Is.Empty); + }); + } + + [TestCase(-1)] + [TestCase(1)] + public void Indexer_SingleValueInvalidIndex_ThrowsException(int index) + { + var values = new HtmxFieldValues("value"); + + Assert.Throws(() => _ = values[index]); + } + + [TestCase(-1)] + [TestCase(2)] + public void Indexer_ArrayInvalidIndex_ThrowsException(int index) + { + var values = new HtmxFieldValues(["one", "two"]); + + Assert.Throws(() => _ = values[index]); + } + + [Test] + public void Enumeration_GenericAndNonGenericInterfaces_ReturnValuesInOrder() + { + HtmxFieldValues values = ["first", "second", "third"]; + + // ReSharper disable once RedundantCast + var generic = ((IEnumerable)values).ToArray(); + + // ReSharper disable once RedundantCast + var nonGeneric = ((IEnumerable)values).Cast().ToArray(); + + Assert.Multiple(() => + { + Assert.That(generic, Is.EqualTo(["first", "second", "third"])); + Assert.That(nonGeneric, Is.EqualTo(generic)); + }); + } + + [Test] + public void Enumerator_AfterSequenceEnds_RemainsCompleted() + { + var enumerator = new HtmxFieldValues.Enumerator(["first", "second"]); + + Assert.Multiple(() => + { + Assert.That(enumerator.MoveNext(), Is.True); + Assert.That(enumerator.Current, Is.EqualTo("first")); + Assert.That(enumerator.MoveNext(), Is.True); + Assert.That(enumerator.Current, Is.EqualTo("second")); + Assert.That(enumerator.MoveNext(), Is.False); + Assert.That(enumerator.MoveNext(), Is.False); + }); + } + + [Test] + public void Enumerator_Reset_ThrowsNotSupportedException() + { + IEnumerator enumerator = new HtmxFieldValues.Enumerator("value"); + + Assert.Throws(enumerator.Reset); + } + + [Test] + public void JsonSerialization_EmptySingleAndMultipleValues_UsesExpectedShapes() + { + HtmxFieldValues single = "a\"b"; + HtmxFieldValues multiple = ["first", "second"]; + + Assert.Multiple(() => + { + Assert.That(JsonSerializer.Serialize(default(HtmxFieldValues)), Is.EqualTo("[]")); + Assert.That(JsonSerializer.Serialize(single), Is.EqualTo("\"a\\u0022b\"")); + Assert.That(JsonSerializer.Serialize(multiple), Is.EqualTo("[\"first\",\"second\"]")); + }); + } +} diff --git a/tests/Ramstack.HtmxToolkit.Tests/HtmxResponseTests.cs b/tests/Ramstack.HtmxToolkit.Tests/HtmxResponseTests.cs index 0adf4a3..40860b6 100644 --- a/tests/Ramstack.HtmxToolkit.Tests/HtmxResponseTests.cs +++ b/tests/Ramstack.HtmxToolkit.Tests/HtmxResponseTests.cs @@ -13,13 +13,12 @@ public void Location_SetsHeader() } [Test] - public void Location_WithContext_SerializesJson() + public void Location_WithOptions_SerializesJson() { var context = TestHelper.CreateHtmxRequestContext(); - context.Response.Htmx(r => r.Location("/bar", new AjaxContext + context.Response.Htmx(r => r.Location("/bar", new HtmxLocationOptions { Source = "button", - Event = "click", Target = "#content", Swap = HtmxSwap.OuterHtml, Select = "#list" @@ -30,17 +29,16 @@ public void Location_WithContext_SerializesJson() Assert.That(json["path"].GetString(), Is.EqualTo("/bar")); Assert.That(json["source"].GetString(), Is.EqualTo("button")); - Assert.That(json["event"].GetString(), Is.EqualTo("click")); Assert.That(json["target"].GetString(), Is.EqualTo("#content")); Assert.That(json["swap"].GetString(), Is.EqualTo("outerHTML")); Assert.That(json["select"].GetString(), Is.EqualTo("#list")); } [Test] - public void Location_WithContext_OmitsNullProperties() + public void Location_WithOptions_OmitsNullProperties() { var context = TestHelper.CreateHtmxRequestContext(); - context.Response.Htmx(r => r.Location("/bar", new AjaxContext())); + context.Response.Htmx(r => r.Location("/bar", new HtmxLocationOptions())); var header = context.Response.Headers[HtmxResponseHeaderNames.Location].ToString(); var json = JsonHelper.ParseJson(header); @@ -51,24 +49,49 @@ public void Location_WithContext_OmitsNullProperties() } [Test] - public void Location_WithContext_SerializesHandlerValuesAndHeaders() + public void Location_WithOptions_SerializesValuesAndHeaders() { var context = TestHelper.CreateHtmxRequestContext(); - context.Response.Htmx(r => r.Location("/bar", new AjaxContext + context.Response.Htmx(r => r.Location("/bar", new HtmxLocationOptions { - Handler = "handleResponse", - Values = new Dictionary { ["id"] = 42 }, - Headers = new Dictionary { ["X-Test"] = "abc" } + Values = new Dictionary + { + ["id"] = "42", + ["tags"] = ["dotnet", "web"] + }, + Headers = new Dictionary + { + ["X-Test"] = "abc" + } })); var header = context.Response.Headers[HtmxResponseHeaderNames.Location].ToString(); var json = JsonHelper.ParseJson(header); - Assert.That(json["handler"].GetString(), Is.EqualTo("handleResponse")); - Assert.That(json["values"].GetProperty("id").GetInt32(), Is.EqualTo(42)); + Assert.That(json["values"].GetProperty("id").GetString(), Is.EqualTo("42")); + Assert.That(json["values"].GetProperty("tags").GetRawText(), Is.EqualTo("[\"dotnet\",\"web\"]")); Assert.That(json["headers"].GetProperty("X-Test").GetString(), Is.EqualTo("abc")); } + [Test] + public void Location_WithOptions_SerializesHistoryAndOutOfBandOptions() + { + var context = TestHelper.CreateHtmxRequestContext(); + context.Response.Htmx(r => r.Location("/bar", new HtmxLocationOptions + { + SelectOob = "#alerts", + Push = "false", + Replace = "/replaced" + })); + + var header = context.Response.Headers[HtmxResponseHeaderNames.Location].ToString(); + var json = JsonHelper.ParseJson(header); + + Assert.That(json["selectOOB"].GetString(), Is.EqualTo("#alerts")); + Assert.That(json["push"].GetString(), Is.EqualTo("false")); + Assert.That(json["replace"].GetString(), Is.EqualTo("/replaced")); + } + [Test] public void PushUrl_SetsHeader() {