diff --git a/InertiaCore/Inertia.cs b/InertiaCore/Inertia.cs index 9220dc6..d6e538d 100644 --- a/InertiaCore/Inertia.cs +++ b/InertiaCore/Inertia.cs @@ -35,7 +35,21 @@ public static class Inertia public static AlwaysProp Always(Func> callback) => _factory.Always(callback); + public static DeferProp Defer(Func callback, string group = "default") => _factory.Defer(callback, group); + + public static DeferProp Defer(Func> callback, string group = "default") => _factory.Defer(callback, group); + public static LazyProp Lazy(Func callback) => _factory.Lazy(callback); public static LazyProp Lazy(Func> callback) => _factory.Lazy(callback); + + public static OptionalProp Optional(Func callback) => _factory.Optional(callback); + + public static OptionalProp Optional(Func> callback) => _factory.Optional(callback); + + public static MergeProp Merge(object? value) => _factory.Merge(value); + + public static MergeProp Merge(Func callback) => _factory.Merge(callback); + + public static MergeProp Merge(Func> callback) => _factory.Merge(callback); } diff --git a/InertiaCore/Models/Page.cs b/InertiaCore/Models/Page.cs index 47abba7..b94ff10 100644 --- a/InertiaCore/Models/Page.cs +++ b/InertiaCore/Models/Page.cs @@ -1,3 +1,5 @@ +using System.Text.Json.Serialization; + namespace InertiaCore.Models; internal class Page @@ -6,4 +8,10 @@ internal class Page public string Component { get; set; } = default!; public string? Version { get; set; } public string Url { get; set; } = default!; + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public List? MergeProps { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public Dictionary>? DeferredProps { get; set; } } diff --git a/InertiaCore/Props/DeferProp.cs b/InertiaCore/Props/DeferProp.cs new file mode 100644 index 0000000..0d421ac --- /dev/null +++ b/InertiaCore/Props/DeferProp.cs @@ -0,0 +1,36 @@ +using InertiaCore.Utils; + +namespace InertiaCore.Props; + +public class DeferProp : InvokableProp, IIgnoresFirstLoad, Mergeable +{ + public bool merge { get; set; } + protected readonly string _group = "default"; + + public DeferProp(object? value, string group) : base(value) + { + _group = group; + } + + internal DeferProp(Func value, string group) : base(value) + { + _group = group; + } + + internal DeferProp(Func> value, string group) : base(value) + { + _group = group; + } + + public Mergeable Merge() + { + merge = true; + + return this; + } + + public string? Group() + { + return _group; + } +} diff --git a/InertiaCore/Props/LazyProp.cs b/InertiaCore/Props/LazyProp.cs index 6e3e958..11bad35 100644 --- a/InertiaCore/Props/LazyProp.cs +++ b/InertiaCore/Props/LazyProp.cs @@ -1,6 +1,8 @@ +using InertiaCore.Utils; + namespace InertiaCore.Props; -public class LazyProp : InvokableProp +public class LazyProp : InvokableProp, IIgnoresFirstLoad { internal LazyProp(Func value) : base(value) { diff --git a/InertiaCore/Props/MergeProp.cs b/InertiaCore/Props/MergeProp.cs new file mode 100644 index 0000000..6d41883 --- /dev/null +++ b/InertiaCore/Props/MergeProp.cs @@ -0,0 +1,25 @@ +using InertiaCore.Props; + +namespace InertiaCore.Utils; + +public class MergeProp : InvokableProp, Mergeable +{ + public bool merge { get; set; } = true; + + public MergeProp(object? value) : base(value) + { + merge = true; + } + + internal MergeProp(Func value) : base(value) + { + merge = true; + } + + internal MergeProp(Func> value) : base(value) + { + merge = true; + } +} + + diff --git a/InertiaCore/Props/OptionalProp.cs b/InertiaCore/Props/OptionalProp.cs new file mode 100644 index 0000000..cf9c971 --- /dev/null +++ b/InertiaCore/Props/OptionalProp.cs @@ -0,0 +1,14 @@ +using InertiaCore.Utils; + +namespace InertiaCore.Props; + +public class OptionalProp : InvokableProp, IIgnoresFirstLoad +{ + internal OptionalProp(Func value) : base(value) + { + } + + internal OptionalProp(Func> value) : base(value) + { + } +} diff --git a/InertiaCore/Response.cs b/InertiaCore/Response.cs index 4b9ed72..621cd10 100644 --- a/InertiaCore/Response.cs +++ b/InertiaCore/Response.cs @@ -43,6 +43,8 @@ protected internal async Task ProcessResponse() Props = props }; + page.MergeProps = ResolveMergeProps(props); + page.DeferredProps = ResolveDeferredProps(props); page.Props["errors"] = GetErrors(); SetPage(page); @@ -84,7 +86,7 @@ protected internal async Task ProcessResponse() if (!isPartial) return props - .Where(kv => kv.Value is not LazyProp) + .Where(kv => kv.Value is not IIgnoresFirstLoad) .ToDictionary(kv => kv.Key, kv => kv.Value); props = props.ToDictionary(kv => kv.Key, kv => kv.Value); @@ -140,6 +142,74 @@ protected internal async Task ProcessResponse() .Concat(alwaysProps).ToDictionary(kv => kv.Key, kv => kv.Value); } + /// + /// Resolve `merge` properties that should be appended to the existing values by the front-end. + /// + private List? ResolveMergeProps(Dictionary props) + { + // Parse the "RESET" header into a collection of keys to reset + var resetProps = new HashSet( + _context!.HttpContext.Request.Headers[InertiaHeader.Reset] + .ToString() + .Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) + .Select(s => s.Trim()), + StringComparer.OrdinalIgnoreCase + ); + + var resolvedProps = props + .Select(kv => kv.Key.ToCamelCase()) // Convert property name to camelCase + .ToList(); + + // Filter the props that are Mergeable and should be merged + var mergeProps = _props.Where(o => o.Value is Mergeable mergeable && mergeable.ShouldMerge()) // Check if value is Mergeable and should merge + .Where(kv => !resetProps.Contains(kv.Key)) // Exclude reset keys + .Select(kv => kv.Key.ToCamelCase()) // Convert property name to camelCase + .Where(resolvedProps.Contains) // Filter only the props that are in the resolved props + .ToList(); + + if (mergeProps.Count == 0) + { + return null; + } + + // Return the result + return mergeProps; + } + + /// + /// Resolve `deferred` properties that should be fetched after the initial page load. + /// + private Dictionary>? ResolveDeferredProps(Dictionary props) + { + + bool isPartial = _context!.IsInertiaPartialComponent(_component); + if (isPartial) + { + return null; + } + + var deferredProps = _props.Where(o => o.Value is DeferProp) // Filter props that are instances of DeferProp + .Select(kv => new + { + Key = kv.Key, + Group = ((DeferProp)kv.Value!).Group() + }) // Map each prop to a new object with Key and Group + + .GroupBy(x => x.Group) // Group by 'Group' + .ToDictionary( + g => g.Key!, + g => g.Select(x => x.Key.ToCamelCase()).ToList() // Extract 'Key' for each group + ); + + if (deferredProps.Count == 0) + { + return null; + } + + // Return the result + return deferredProps; + } + /// /// Resolve all necessary class instances in the given props. /// diff --git a/InertiaCore/ResponseFactory.cs b/InertiaCore/ResponseFactory.cs index 8bce7cf..fe25cae 100644 --- a/InertiaCore/ResponseFactory.cs +++ b/InertiaCore/ResponseFactory.cs @@ -26,6 +26,13 @@ internal interface IResponseFactory public AlwaysProp Always(Func> callback); public LazyProp Lazy(Func callback); public LazyProp Lazy(Func> callback); + public DeferProp Defer(Func callback, string group = "default"); + public DeferProp Defer(Func> callback, string group = "default"); + public MergeProp Merge(object? value); + public MergeProp Merge(Func callback); + public MergeProp Merge(Func> callback); + public OptionalProp Optional(Func callback); + public OptionalProp Optional(Func> callback); } internal class ResponseFactory : IResponseFactory @@ -127,10 +134,16 @@ public void Share(IDictionary data) context.Features.Set(sharedData); } - public LazyProp Lazy(Func callback) => new(callback); public LazyProp Lazy(Func> callback) => new(callback); public AlwaysProp Always(object? value) => new(value); public AlwaysProp Always(Func callback) => new(callback); public AlwaysProp Always(Func> callback) => new(callback); + public DeferProp Defer(Func callback, string group = "default") => new(callback, group); + public DeferProp Defer(Func> callback, string group = "default") => new(callback, group); + public MergeProp Merge(object? value) => new(value); + public MergeProp Merge(Func callback) => new(callback); + public MergeProp Merge(Func> callback) => new(callback); + public OptionalProp Optional(Func callback) => new(callback); + public OptionalProp Optional(Func> callback) => new(callback); } diff --git a/InertiaCore/Utils/IIgnoresFirstLoad.cs b/InertiaCore/Utils/IIgnoresFirstLoad.cs new file mode 100644 index 0000000..10fc9ba --- /dev/null +++ b/InertiaCore/Utils/IIgnoresFirstLoad.cs @@ -0,0 +1,5 @@ +namespace InertiaCore.Utils; + +public interface IIgnoresFirstLoad +{ +} diff --git a/InertiaCore/Utils/InertiaHeader.cs b/InertiaCore/Utils/InertiaHeader.cs index 80de7d8..b75e6cf 100644 --- a/InertiaCore/Utils/InertiaHeader.cs +++ b/InertiaCore/Utils/InertiaHeader.cs @@ -15,4 +15,6 @@ public static class InertiaHeader public const string PartialOnly = "X-Inertia-Partial-Data"; public const string PartialExcept = "X-Inertia-Partial-Except"; + + public const string Reset = "X-Inertia-Reset"; } diff --git a/InertiaCore/Utils/Mergeable.cs b/InertiaCore/Utils/Mergeable.cs new file mode 100644 index 0000000..b7e5b6e --- /dev/null +++ b/InertiaCore/Utils/Mergeable.cs @@ -0,0 +1,15 @@ +namespace InertiaCore.Utils; + +public interface Mergeable +{ + public bool merge { get; set; } + + public Mergeable Merge() + { + merge = true; + + return this; + } + + public bool ShouldMerge() => merge; +} diff --git a/InertiaCoreTests/UnitTestDeferData.cs b/InertiaCoreTests/UnitTestDeferData.cs new file mode 100644 index 0000000..67772d5 --- /dev/null +++ b/InertiaCoreTests/UnitTestDeferData.cs @@ -0,0 +1,326 @@ +using InertiaCore.Models; +using Microsoft.AspNetCore.Http; + +namespace InertiaCoreTests; + +public partial class Tests +{ + [Test] + [Description("Test if the defer data is fetched properly.")] + public async Task TestDeferData() + { + var response = _factory.Render("Test/Page", new + { + Test = "Test", + TestFunc = new Func(() => "Func"), + TestDefer = _factory.Defer(() => + { + return "Defer"; + }) + }); + + var context = PrepareContext(); + + response.SetContext(context); + await response.ProcessResponse(); + + var page = response.GetJson().Value as Page; + + Assert.That(page?.Props, Is.EqualTo(new Dictionary + { + { "test", "Test" }, + { "testFunc", "Func" }, + { "errors", new Dictionary(0) } + })); + Assert.That(page?.DeferredProps, Is.EqualTo(new Dictionary> { + { "default", new List { "testDefer" } } + })); + Assert.That(page?.MergeProps, Is.EqualTo(null)); + } + + [Test] + [Description("Test if the defer data is fetched properly with specified partial props.")] + public async Task TestDeferPartialData() + { + var response = _factory.Render("Test/Page", new + { + TestFunc = new Func(() => "Func"), + TestDefer = _factory.Defer(() => "Deferred") + }); + + var headers = new HeaderDictionary + { + { "X-Inertia-Partial-Data", "testFunc,testDefer" }, + { "X-Inertia-Partial-Component", "Test/Page" } + }; + + var context = PrepareContext(headers); + + response.SetContext(context); + await response.ProcessResponse(); + + var page = response.GetJson().Value as Page; + + Assert.That(page?.Props, Is.EqualTo(new Dictionary + { + { "testFunc", "Func" }, + { "testDefer", "Deferred" }, + { "errors", new Dictionary(0) } + })); + + Assert.That(page?.DeferredProps, Is.EqualTo(null)); + Assert.That(page?.MergeProps, Is.EqualTo(null)); + } + + [Test] + [Description("Test if the defer/merge data is fetched properly with specified partial props.")] + public async Task TestDeferMergePartialData() + { + var response = _factory.Render("Test/Page", new + { + TestFunc = new Func(() => "Func"), + TestDefer = _factory.Defer(() => "Deferred").Merge() + }); + + var headers = new HeaderDictionary + { + { "X-Inertia-Partial-Data", "testFunc,testDefer" }, + { "X-Inertia-Partial-Component", "Test/Page" } + }; + + var context = PrepareContext(headers); + + response.SetContext(context); + await response.ProcessResponse(); + + var page = response.GetJson().Value as Page; + + Assert.That(page?.Props, Is.EqualTo(new Dictionary + { + { "testFunc", "Func" }, + { "testDefer", "Deferred" }, + { "errors", new Dictionary(0) } + })); + + Assert.That(page?.DeferredProps, Is.EqualTo(null)); + Assert.That(page?.MergeProps, Is.EqualTo(new List { "testDefer" })); + } + + [Test] + [Description("Test if the defer async data is fetched properly.")] + public async Task TestDeferAsyncData() + { + var testFunction = new Func>(async () => + { + await Task.Delay(100); + return "Defer Async"; + }); + + var response = _factory.Render("Test/Page", new + { + Test = "Test", + TestFunc = new Func(() => "Func"), + TestDefer = _factory.Defer(testFunction) + }); + + var context = PrepareContext(); + + response.SetContext(context); + await response.ProcessResponse(); + + var page = response.GetJson().Value as Page; + + Assert.That(page?.Props, Is.EqualTo(new Dictionary + { + { "test", "Test" }, + { "testFunc", "Func" }, + { "errors", new Dictionary(0) } + })); + Assert.That(page?.DeferredProps, Is.EqualTo(new Dictionary> { + { "default", new List { "testDefer" } } + })); + Assert.That(page?.MergeProps, Is.EqualTo(null)); + } + + [Test] + [Description("Test if the defer async data is fetched properly with specified partial props.")] + public async Task TestDeferAsyncPartialData() + { + var testFunction = new Func>(async () => + { + await Task.Delay(100); + return "Defer Async"; + }); + + var response = _factory.Render("Test/Page", new + { + TestFunc = new Func(() => "Func"), + TestDefer = _factory.Defer(async () => await testFunction()) + }); + + var headers = new HeaderDictionary + { + { "X-Inertia-Partial-Data", "testFunc,testDefer" }, + { "X-Inertia-Partial-Component", "Test/Page" } + }; + + var context = PrepareContext(headers); + + response.SetContext(context); + await response.ProcessResponse(); + + var page = response.GetJson().Value as Page; + + Assert.That(page?.Props, Is.EqualTo(new Dictionary + { + { "testFunc", "Func" }, + { "testDefer", "Defer Async" }, + { "errors", new Dictionary(0) } + })); + + Assert.That(page?.DeferredProps, Is.EqualTo(null)); + Assert.That(page?.MergeProps, Is.EqualTo(null)); + } + + [Test] + [Description("Test if the defer & merge async data is fetched properly with specified partial props.")] + public async Task TestDeferMergeAsyncPartialData() + { + var testFunction = new Func>(async () => + { + await Task.Delay(100); + return "Defer Async"; + }); + + var response = _factory.Render("Test/Page", new + { + TestFunc = new Func(() => "Func"), + TestDefer = _factory.Defer(async () => await testFunction()).Merge() + }); + + var headers = new HeaderDictionary + { + { "X-Inertia-Partial-Data", "testFunc,testDefer" }, + { "X-Inertia-Partial-Component", "Test/Page" } + }; + + var context = PrepareContext(headers); + + response.SetContext(context); + await response.ProcessResponse(); + + var page = response.GetJson().Value as Page; + + Assert.That(page?.Props, Is.EqualTo(new Dictionary + { + { "testFunc", "Func" }, + { "testDefer", "Defer Async" }, + { "errors", new Dictionary(0) } + })); + + Assert.That(page?.DeferredProps, Is.EqualTo(null)); + Assert.That(page?.MergeProps, Is.EqualTo(new List { "testDefer" })); + } + + [Test] + [Description("Test if the defer async data is fetched properly without specified partial props.")] + public async Task TestDeferAsyncPartialDataOmitted() + { + var testFunction = new Func>(async () => + { + await Task.Delay(100); + return "Defer Async"; + }); + + var response = _factory.Render("Test/Page", new + { + TestFunc = new Func(() => "Func"), + TestDefer = _factory.Defer(async () => await testFunction()) + }); + + var headers = new HeaderDictionary + { + { "X-Inertia-Partial-Data", "testFunc" }, + { "X-Inertia-Partial-Component", "Test/Page" } + }; + + var context = PrepareContext(headers); + + response.SetContext(context); + await response.ProcessResponse(); + + var page = response.GetJson().Value as Page; + + Assert.That(page?.Props, Is.EqualTo(new Dictionary + { + { "testFunc", "Func" }, + { "errors", new Dictionary(0) } + })); + + Assert.That(page?.DeferredProps, Is.EqualTo(null)); + Assert.That(page?.MergeProps, Is.EqualTo(null)); + } + + public async Task TestNoDeferredProps() + { + var response = _factory.Render("Test/Page", new + { + Test = "Test", + TestFunc = new Func(() => "Func"), + }); + + var context = PrepareContext(); + + response.SetContext(context); + await response.ProcessResponse(); + + var page = response.GetJson().Value as Page; + + Assert.That(page?.Props, Is.EqualTo(new Dictionary + { + { "test", "Test" }, + { "testFunc", "Func" }, + { "errors", new Dictionary(0) } + })); + Assert.That(page?.DeferredProps, Is.EqualTo(null)); + Assert.That(page?.MergeProps, Is.EqualTo(null)); + } + + [Test] + [Description("Test if the defer data with multiple groups is fetched properly.")] + public async Task TestDeferMultipleGroupsData() + { + var response = _factory.Render("Test/Page", new + { + Test = "Test", + TestFunc = new Func(() => "Func"), + TestDefer = _factory.Defer(() => + { + return "Defer"; + }), + TestStats = _factory.Defer(() => + { + return "Stat"; + }, "stats") + }); + + var context = PrepareContext(); + + response.SetContext(context); + await response.ProcessResponse(); + + var page = response.GetJson().Value as Page; + + Assert.That(page?.Props, Is.EqualTo(new Dictionary + { + { "test", "Test" }, + { "testFunc", "Func" }, + { "errors", new Dictionary(0) } + })); + Assert.That(page?.DeferredProps, Is.EqualTo(new Dictionary> { + { "default", new List { "testDefer" } }, + { "stats", new List { "testStats" } }, + })); + Assert.That(page?.MergeProps, Is.EqualTo(null)); + } +} diff --git a/InertiaCoreTests/UnitTestMergeData.cs b/InertiaCoreTests/UnitTestMergeData.cs new file mode 100644 index 0000000..7065f8d --- /dev/null +++ b/InertiaCoreTests/UnitTestMergeData.cs @@ -0,0 +1,209 @@ +using InertiaCore.Models; +using Microsoft.AspNetCore.Http; + +namespace InertiaCoreTests; + +public partial class Tests +{ + [Test] + [Description("Test if the merge data is fetched properly.")] + public async Task TestMergeData() + { + var response = _factory.Render("Test/Page", new + { + Test = "Test", + TestFunc = new Func(() => "Func"), + TestMerge = _factory.Merge(() => + { + return "Merge"; + }) + }); + + var context = PrepareContext(); + + response.SetContext(context); + await response.ProcessResponse(); + + var page = response.GetJson().Value as Page; + + Assert.That(page?.Props, Is.EqualTo(new Dictionary + { + { "test", "Test" }, + { "testFunc", "Func" }, + { "testMerge", "Merge" }, + { "errors", new Dictionary(0) } + })); + Assert.That(page?.MergeProps, Is.EqualTo(new List { "testMerge" })); + } + + [Test] + [Description("Test if the merge data is fetched properly with specified partial props.")] + public async Task TestMergePartialData() + { + var response = _factory.Render("Test/Page", new + { + TestFunc = new Func(() => "Func"), + TestMerge = _factory.Merge(() => "Merge") + }); + + var headers = new HeaderDictionary + { + { "X-Inertia-Partial-Data", "testFunc,testMerge" }, + { "X-Inertia-Partial-Component", "Test/Page" } + }; + + var context = PrepareContext(headers); + + response.SetContext(context); + await response.ProcessResponse(); + + var page = response.GetJson().Value as Page; + + Assert.That(page?.Props, Is.EqualTo(new Dictionary + { + { "testFunc", "Func" }, + { "testMerge", "Merge" }, + { "errors", new Dictionary(0) } + })); + + Assert.That(page?.MergeProps, Is.EqualTo(new List { "testMerge" })); + } + + [Test] + [Description("Test if the merge async data is fetched properly.")] + public async Task TestMergeAsyncData() + { + var testFunction = new Func>(async () => + { + await Task.Delay(100); + return "Merge Async"; + }); + + var response = _factory.Render("Test/Page", new + { + Test = "Test", + TestFunc = new Func(() => "Func"), + TestMerge = _factory.Merge(testFunction) + }); + + var context = PrepareContext(); + + response.SetContext(context); + await response.ProcessResponse(); + + var page = response.GetJson().Value as Page; + + Assert.That(page?.Props, Is.EqualTo(new Dictionary + { + { "test", "Test" }, + { "testFunc", "Func" }, + { "testMerge", "Merge Async" }, + { "errors", new Dictionary(0) } + })); + Assert.That(page?.MergeProps, Is.EqualTo(new List { "testMerge" })); + } + + [Test] + [Description("Test if the merge async data is fetched properly with specified partial props.")] + public async Task TestMergeAsyncPartialData() + { + var testFunction = new Func>(async () => + { + await Task.Delay(100); + return "Merge Async"; + }); + + var response = _factory.Render("Test/Page", new + { + TestFunc = new Func(() => "Func"), + TestMerge = _factory.Merge(async () => await testFunction()) + }); + + var headers = new HeaderDictionary + { + { "X-Inertia-Partial-Data", "testFunc,testMerge" }, + { "X-Inertia-Partial-Component", "Test/Page" } + }; + + var context = PrepareContext(headers); + + response.SetContext(context); + await response.ProcessResponse(); + + var page = response.GetJson().Value as Page; + + Assert.That(page?.Props, Is.EqualTo(new Dictionary + { + { "testFunc", "Func" }, + { "testMerge", "Merge Async" }, + { "errors", new Dictionary(0) } + })); + + Assert.That(page?.MergeProps, Is.EqualTo(new List { "testMerge" })); + } + + [Test] + [Description("Test if the merge async data is fetched properly without specified partial props.")] + public async Task TestMergeAsyncPartialDataOmitted() + { + var testFunction = new Func>(async () => + { + await Task.Delay(100); + return "Merge Async"; + }); + + var response = _factory.Render("Test/Page", new + { + TestFunc = new Func(() => "Func"), + TestMerge = _factory.Merge(async () => await testFunction()) + }); + + var headers = new HeaderDictionary + { + { "X-Inertia-Partial-Data", "testFunc" }, + { "X-Inertia-Partial-Component", "Test/Page" } + }; + + var context = PrepareContext(headers); + + response.SetContext(context); + await response.ProcessResponse(); + + var page = response.GetJson().Value as Page; + + Assert.That(page?.Props, Is.EqualTo(new Dictionary + { + { "testFunc", "Func" }, + { "errors", new Dictionary(0) } + })); + + Assert.That(page?.MergeProps, Is.EqualTo(null)); + } + + [Test] + [Description("Test if the merge async data is fetched properly without specified partial props.")] + public async Task TestNoMergeProps() + { + var response = _factory.Render("Test/Page", new + { + Test = "Test", + TestFunc = new Func(() => "Func"), + }); + + var context = PrepareContext(); + + response.SetContext(context); + await response.ProcessResponse(); + + var page = response.GetJson().Value as Page; + + Assert.That(page?.Props, Is.EqualTo(new Dictionary + { + { "test", "Test" }, + { "testFunc", "Func" }, + { "errors", new Dictionary(0) } + })); + Assert.That(page?.MergeProps, Is.EqualTo(null)); + } + +} diff --git a/InertiaCoreTests/UnitTestOptionalData.cs b/InertiaCoreTests/UnitTestOptionalData.cs new file mode 100644 index 0000000..3d925ec --- /dev/null +++ b/InertiaCoreTests/UnitTestOptionalData.cs @@ -0,0 +1,139 @@ +using InertiaCore.Models; +using Microsoft.AspNetCore.Http; + +namespace InertiaCoreTests; + +public partial class Tests +{ + [Test] + [Description("Test if the optional data is fetched properly.")] + public async Task TestOptionalData() + { + var response = _factory.Render("Test/Page", new + { + Test = "Test", + TestFunc = new Func(() => "Func"), + TestOptional = _factory.Optional(() => + { + Assert.Fail(); + return "Optional"; + }) + }); + + var context = PrepareContext(); + + response.SetContext(context); + await response.ProcessResponse(); + + var page = response.GetJson().Value as Page; + + Assert.That(page?.Props, Is.EqualTo(new Dictionary + { + { "test", "Test" }, + { "testFunc", "Func" }, + { "errors", new Dictionary(0) } + })); + } + + [Test] + [Description("Test if the optional data is fetched properly with specified partial props.")] + public async Task TestOptionalPartialData() + { + var response = _factory.Render("Test/Page", new + { + TestFunc = new Func(() => "Func"), + TestOptional = _factory.Optional(() => "Optional") + }); + + var headers = new HeaderDictionary + { + { "X-Inertia-Partial-Data", "testFunc,testOptional" }, + { "X-Inertia-Partial-Component", "Test/Page" } + }; + + var context = PrepareContext(headers); + + response.SetContext(context); + await response.ProcessResponse(); + + var page = response.GetJson().Value as Page; + + Assert.That(page?.Props, Is.EqualTo(new Dictionary + { + { "testFunc", "Func" }, + { "testOptional", "Optional" }, + { "errors", new Dictionary(0) } + })); + } + + + [Test] + [Description("Test if the optional async data is fetched properly.")] + public async Task TestOptionalAsyncData() + { + var testFunction = new Func>(async () => + { + Assert.Fail(); + await Task.Delay(100); + return "Optional Async"; + }); + + var response = _factory.Render("Test/Page", new + { + Test = "Test", + TestFunc = new Func(() => "Func"), + TestOptional = _factory.Optional(testFunction) + }); + + var context = PrepareContext(); + + response.SetContext(context); + await response.ProcessResponse(); + + var page = response.GetJson().Value as Page; + + Assert.That(page?.Props, Is.EqualTo(new Dictionary + { + { "test", "Test" }, + { "testFunc", "Func" }, + { "errors", new Dictionary(0) } + })); + } + + [Test] + [Description("Test if the optional async data is fetched properly with specified partial props.")] + public async Task TestOptionalAsyncPartialData() + { + var testFunction = new Func>(async () => + { + await Task.Delay(100); + return "Optional Async"; + }); + + var response = _factory.Render("Test/Page", new + { + TestFunc = new Func(() => "Func"), + TestOptional = _factory.Optional(async () => await testFunction()) + }); + + var headers = new HeaderDictionary + { + { "X-Inertia-Partial-Data", "testFunc,testOptional" }, + { "X-Inertia-Partial-Component", "Test/Page" } + }; + + var context = PrepareContext(headers); + + response.SetContext(context); + await response.ProcessResponse(); + + var page = response.GetJson().Value as Page; + + Assert.That(page?.Props, Is.EqualTo(new Dictionary + { + { "testFunc", "Func" }, + { "testOptional", "Optional Async" }, + { "errors", new Dictionary(0) } + })); + } +} diff --git a/InertiaCoreTests/UnitTestResult.cs b/InertiaCoreTests/UnitTestResult.cs index a88e79d..48c6db1 100644 --- a/InertiaCoreTests/UnitTestResult.cs +++ b/InertiaCoreTests/UnitTestResult.cs @@ -1,6 +1,8 @@ using InertiaCore.Models; +using InertiaCore.Utils; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using System.Text.Json; namespace InertiaCoreTests; @@ -40,6 +42,162 @@ public async Task TestJsonResult() { "test", "Test" }, { "errors", new Dictionary(0) } })); + + // Check the serialized JSON + var jsonString = JsonSerializer.Serialize(json); + var dictionary = JsonSerializer.Deserialize>(jsonString); + + Assert.That(dictionary, Is.Not.Null); + Assert.That(dictionary!.ContainsKey("MergeProps"), Is.False); + Assert.That(dictionary!.ContainsKey("DeferredProps"), Is.False); + }); + } + + [Test] + [Description("Test if the JSON result with merged data is created correctly.")] + public async Task TestJsonMergedResult() + { + var response = _factory.Render("Test/Page", new + { + Test = "Test", + TestMerged = _factory.Merge(() => "Merged") + }); + + var headers = new HeaderDictionary + { + { "X-Inertia", "true" } + }; + + var context = PrepareContext(headers); + + response.SetContext(context); + await response.ProcessResponse(); + + var result = response.GetResult(); + + Assert.Multiple(() => + { + Assert.That(result, Is.InstanceOf(typeof(JsonResult))); + + var json = (result as JsonResult)?.Value; + Assert.That(json, Is.InstanceOf(typeof(Page))); + + Assert.That((json as Page)?.Component, Is.EqualTo("Test/Page")); + Assert.That((json as Page)?.Props, Is.EqualTo(new Dictionary + { + { "test", "Test" }, + { "testMerged", "Merged" }, + { "errors", new Dictionary(0) } + })); + Assert.That((json as Page)?.MergeProps, Is.EqualTo(new List { + "testMerged" + })); + + // Check the serialized JSON + var jsonString = JsonSerializer.Serialize(json); + var dictionary = JsonSerializer.Deserialize>(jsonString); + + Assert.That(dictionary, Is.Not.Null); + Assert.That(dictionary!.ContainsKey("MergeProps"), Is.True); + }); + } + + [Test] + [Description("Test if the JSON result with deferred data is created correctly.")] + public async Task TestJsonDeferredResult() + { + var response = _factory.Render("Test/Page", new + { + Test = "Test", + TestDeferred = _factory.Defer(() => "Deferred") + }); + + var headers = new HeaderDictionary + { + { "X-Inertia", "true" } + }; + + var context = PrepareContext(headers); + + response.SetContext(context); + await response.ProcessResponse(); + + var result = response.GetResult(); + + Assert.Multiple(() => + { + Assert.That(result, Is.InstanceOf(typeof(JsonResult))); + + var json = (result as JsonResult)?.Value; + Assert.That(json, Is.InstanceOf(typeof(Page))); + + Assert.That((json as Page)?.Component, Is.EqualTo("Test/Page")); + Assert.That((json as Page)?.Props, Is.EqualTo(new Dictionary + { + { "test", "Test" }, + { "errors", new Dictionary(0) } + })); + Assert.That((json as Page)?.MergeProps, Is.EqualTo(null)); + + // Check the serialized JSON + var jsonString = JsonSerializer.Serialize(json); + var dictionary = JsonSerializer.Deserialize>(jsonString); + + Assert.That(dictionary, Is.Not.Null); + Assert.That(dictionary!.ContainsKey("MergeProps"), Is.False); + Assert.That(dictionary!.ContainsKey("DeferredProps"), Is.True); + }); + } + + [Test] + [Description("Test if the JSON result with deferred & merge data is created correctly.")] + public async Task TestJsonMergeDeferredResult() + { + var response = _factory.Render("Test/Page", new + { + Test = "Test", + TestMerged = _factory.Defer(() => "Merged").Merge(), + }); + + var headers = new HeaderDictionary + { + { InertiaHeader.Inertia, "true" }, + { InertiaHeader.PartialComponent, "Test/Page" }, + { InertiaHeader.PartialOnly, "testMerged" }, + }; + + var context = PrepareContext(headers); + + response.SetContext(context); + await response.ProcessResponse(); + + var result = response.GetResult(); + + Assert.Multiple(() => + { + Assert.That(result, Is.InstanceOf(typeof(JsonResult))); + + var json = (result as JsonResult)?.Value; + Assert.That(json, Is.InstanceOf(typeof(Page))); + + Assert.That((json as Page)?.Component, Is.EqualTo("Test/Page")); + Assert.That((json as Page)?.Props, Is.EqualTo(new Dictionary + { + { "testMerged", "Merged" }, + { "errors", new Dictionary(0) } + })); + Assert.That((json as Page)?.MergeProps, Is.EqualTo(new List { + "testMerged" + })); + Assert.That((json as Page)?.DeferredProps, Is.EqualTo(null)); + + // Check the serialized JSON + var jsonString = JsonSerializer.Serialize(json); + var dictionary = JsonSerializer.Deserialize>(jsonString); + + Assert.That(dictionary, Is.Not.Null); + Assert.That(dictionary!.ContainsKey("MergeProps"), Is.True); + Assert.That(dictionary!.ContainsKey("DeferredProps"), Is.False); }); }