From e0a4b12419443d46f3d74f00bf4004d332bdbb3b Mon Sep 17 00:00:00 2001 From: Ivan Lieckens Date: Thu, 5 Sep 2024 15:15:57 +0200 Subject: [PATCH] Custom Content Resolver Support (#14) ## Description / Motivation This modifies the FieldParser to support Custom Content Resolvers returning modified JSON. This fixes #8 Added missing CONTRIBUTING file. Removed `AddSystemTextJson` which should not be used or needed as it pollutes the DI. ## Testing Added integration tests both for layout client and rendering engine binding leveraging the "Navigation" component from Headless SXA. - [X] The Unit & Intergration tests are passing. - [X] I have added the necesary tests to cover my changes. ## Terms - [X] I agree to follow this project's [Code of Conduct](CODE_OF_CONDUCT.md). --------- Co-authored-by: Ivan Lieckens --- CONTRIBUTING.md | 74 ++++ Sitecore.AspNetCore.SDK.sln | 4 + .../SitecoreLayoutClientBuilderExtensions.cs | 19 +- .../Serialization/Converter/FieldParser.cs | 27 +- .../FieldsFixture.cs | 26 ++ ...outService.Client.Integration.Tests.csproj | 4 + .../Converter/FieldParserTests.cs | 21 +- .../ComponentModels/CustomResolver.cs | 10 + .../ComponentModels/CustomResolverModel.cs | 17 + .../PartialDesignDynamicPlaceholder.cs | 9 + .../Controllers/HomeController.cs | 3 +- .../Binding/CustomResolverBindingFixture.cs | 86 +++++ ...K.RenderingEngine.Integration.Tests.csproj | 7 + .../SitecoreComponent/CustomResolver.cshtml | 24 ++ .../PartialDesignDynamicPlaceholder.cshtml | 4 + .../Views/Shared/HeadlessSxaLayout.cshtml | 12 + .../TestConstants.cs | 2 + tests/data/json/headlessSxa.json | 334 ++++++++++++++++++ 18 files changed, 648 insertions(+), 35 deletions(-) create mode 100644 CONTRIBUTING.md create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/ComponentModels/CustomResolver.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/ComponentModels/CustomResolverModel.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/ComponentModels/PartialDesignDynamicPlaceholder.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/Binding/CustomResolverBindingFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/Shared/Components/SitecoreComponent/CustomResolver.cshtml create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/Shared/Components/SitecoreComponent/PartialDesignDynamicPlaceholder.cshtml create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/Shared/HeadlessSxaLayout.cshtml create mode 100644 tests/data/json/headlessSxa.json diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..ee1a3ae --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,74 @@ +# Contribution to the Sitecore ASP.NET Core SDK + +Thank you for your interest in contributing to our project. You can contribute with issues and PRs. Simply filing issues for problems you encounter is a great way to contribute. Contributing implementations is greatly appreciated. + +## Code of Conduct +Please read the [Code of Conduct](./CODE_OF_CONDUCT.md) before participating, all contributors are expected to uphold this code. Please report unacceptable behavior to [community@sitecore.com](mailto:community@sitecore.com). + +## Reporting Issues + +We always welcome bug reports, feature requests and overall feedback. Here are a few tips on how you can make reporting your issue as effective as possible. + +### Finding Existing Issues + +Before filing a new issue, please search our [open issues](https://github.com/Sitecore/ASP.NET-Core-SDK/issues) to check if it already exists. + +If you do find an existing issue, please include your own feedback in the discussion. Do consider upvoting (reaction) the original post, as this helps us prioritize popular issues. + +### Use the right template + +When creating a new issue, please use the appropriate template. We have the following templates available: + +| Template | Description | +| ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | +| Bug Report | Use this template to report a bug. | +| Feature Request | Use this template to request a new feature. | +| Question | Use this template to ask a question. For implementation specific questions please use [StackExchange](https://sitecore.stackexchange.com/). | +| Report a security vulnerability | Use this template to report a security vulnerability. | + +### Writing a Good Bug Report + +Good bug reports make it easier for maintainers to verify and root cause the underlying problem. The better a bug report, the faster the problem will be resolved. Ideally, a bug report should contain the following information: + +* A high-level description of the problem. +* A _minimal reproduction_, i.e. the smallest size of code/configuration required to reproduce the wrong behavior. +* A description of the _expected behavior_, contrasted with the _actual behavior_ observed. +* Information on the environment: Sitecore XM version, SDK version, etc. +* Additional information, e.g. is it a regression from previous versions? are there any known workarounds? + +#### Why are Minimal Reproductions Important? + +A reproduction lets maintainers verify the presence of a bug, and diagnose the issue using a debugger. A _minimal_ reproduction is the smallest possible application demonstrating that bug. Minimal reproductions are generally preferable since they: + +1. Focus debugging efforts on a simple code snippet, +2. Ensure that the problem is not caused by unrelated dependencies/configuration, +3. Avoid the need to share production codebases. + +#### Are Minimal Reproductions Required? + +In certain cases, creating a minimal reproduction might not be practical (e.g. due to nondeterministic factors, external dependencies). In such cases you would be asked to provide as much information as possible, for example by sharing a memory dump of the failing application. If maintainers are unable to root cause the problem, they might still close the issue as not actionable. While not required, minimal reproductions are strongly encouraged and will significantly improve the chances of your issue being prioritized and fixed by the maintainers. + +#### How to Create a Minimal Reproduction + +The best way to create a minimal reproduction is gradually removing code and dependencies from a reproducing app, until the problem no longer occurs. A good minimal reproduction: + +* Excludes all unnecessary types, methods, code blocks, source files, nuget dependencies and project configurations. +* Contains documentation or code comments illustrating expected vs actual behavior. +* If possible, avoids performing any unneeded IO or system calls. + +## Contributiting Code +All code contributions must be submitted by the standard GitHub pull request flow, or by logging an issue. + +### Accepting Contributions +Sitecore has a _"No commitment"_ approach to this repository. The required functionality of the SDK and related XM Cloud features will be the driver for acceptance decisions. We are open to contributions from the community, but make no commitment to accept or incorporate changes. + +### Guidelines +* Create a [fork of the repository](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/fork-a-repo) +* Create a branch for your changes. Use /feat/ for new features, /fix/ for bug fixes and /new/ for breaking changes. +* We observe strict code style rules, please ensure your changes follow the guidance provided by the analyzers. + * [StyleCop](https://github.com/DotNetAnalyzers/StyleCopAnalyzers) + * [File Scoped Namespaces](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-10.0/file-scoped-namespaces) + * Do not use `var` +* We require all changes to be covered by tests. +* When you are ready to submit your changes, [create a pull request from your fork](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request-from-a-fork) into this repository. +* This repository is configured to use `Squash Merges` for all accepted contributions. This decision has been made to keep the commit history clean and concise. \ No newline at end of file diff --git a/Sitecore.AspNetCore.SDK.sln b/Sitecore.AspNetCore.SDK.sln index f851d2d..fd5cc1b 100644 --- a/Sitecore.AspNetCore.SDK.sln +++ b/Sitecore.AspNetCore.SDK.sln @@ -89,7 +89,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sitecore.AspNetCore.SDK.Tra EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "GitHub", "GitHub", "{5FE82369-DEF2-4136-B74F-6E86DB91050E}" ProjectSection(SolutionItems) = preProject + CODE_OF_CONDUCT.md = CODE_OF_CONDUCT.md + CONTRIBUTING.md = CONTRIBUTING.md + LICENSE.md = LICENSE.md .github\pull_request_template.md = .github\pull_request_template.md + README.md = README.md EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{1706E43D-AC19-4FBB-9BFB-18A8B195580A}" diff --git a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Extensions/SitecoreLayoutClientBuilderExtensions.cs b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Extensions/SitecoreLayoutClientBuilderExtensions.cs index 7aef363..d2696a0 100644 --- a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Extensions/SitecoreLayoutClientBuilderExtensions.cs +++ b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Extensions/SitecoreLayoutClientBuilderExtensions.cs @@ -99,7 +99,7 @@ public static ILayoutRequestHandlerBuilder AddGraph request.Language(defaultLanguage); } }); - return builder.AddHandler(name, (sp) + return builder.AddHandler(name, sp => ActivatorUtilities.CreateInstance( sp, client, sp.GetRequiredService(), sp.GetRequiredService>())); } @@ -151,23 +151,6 @@ public static ISitecoreLayoutClientBuilder WithDefaultRequestOptions(this ISitec return builder; } - /// - /// Configures System.Text.Json specific features such as input and output formatters. - /// - /// The being configured. - /// The so that additional calls can be chained. - public static ISitecoreLayoutClientBuilder AddSystemTextJson(this ISitecoreLayoutClientBuilder builder) - { - ServiceDescriptor descriptor = new(typeof(ISitecoreLayoutSerializer), typeof(JsonLayoutServiceSerializer), ServiceLifetime.Singleton); - builder.Services.Replace(descriptor); - - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - - return builder; - } - /// /// Registers a HTTP request handler for the Sitecore layout service client. /// diff --git a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Serialization/Converter/FieldParser.cs b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Serialization/Converter/FieldParser.cs index f35a6f8..5dc9f2d 100644 --- a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Serialization/Converter/FieldParser.cs +++ b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Serialization/Converter/FieldParser.cs @@ -7,15 +7,32 @@ namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Serialization.Converter; /// public class FieldParser : IFieldParser { + /// + /// Field key for custom content created by Custom Content Resolvers. + /// + // ReSharper disable once MemberCanBePrivate.Global - Must be accessible for people using the SDK. + public const string CustomContentFieldKey = "CustomContent"; + /// public Dictionary ParseFields(ref Utf8JsonReader reader) { - if (reader.TokenType != JsonTokenType.StartObject) + Dictionary result = []; + switch (reader.TokenType) { - throw new JsonException(); + case JsonTokenType.StartObject: + result = ParseStandardFields(ref reader); + break; + default: + result.Add(CustomContentFieldKey, new JsonSerializedField(ParseField(ref reader))); + break; } - Dictionary fields = []; + return result; + } + + private static Dictionary ParseStandardFields(ref Utf8JsonReader reader) + { + Dictionary result = []; while (reader.Read() && reader.TokenType != JsonTokenType.EndObject) { string? key = reader.GetString(); @@ -23,11 +40,11 @@ public Dictionary ParseFields(ref Utf8JsonReader reader) JsonDocument value = ParseField(ref reader); if (key != null) { - fields.Add(key, new JsonSerializedField(value)); + result.Add(key, new JsonSerializedField(value)); } } - return fields; + return result; } private static JsonDocument ParseField(ref Utf8JsonReader reader) diff --git a/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Integration.Tests/FieldsFixture.cs b/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Integration.Tests/FieldsFixture.cs index 9b2f229..be2584d 100644 --- a/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Integration.Tests/FieldsFixture.cs +++ b/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Integration.Tests/FieldsFixture.cs @@ -493,4 +493,30 @@ public void Component_RichTextField_CanBeRead(ISitecoreLayoutSerializer serializ resultField!.Value.Should().Be(expectedField.value.Value); resultField.EditableMarkup.Should().Be(expectedField.editable.Value); } + + [Theory] + [MemberData(nameof(Serializers))] + public void HeadlessSxa_CanBeRead(ISitecoreLayoutSerializer serializer) + { + // Arrange + string json = File.ReadAllText("./Json/headlessSxa.json"); + dynamic jsonModel = JObject.Parse(json); + + // Act + SitecoreLayoutResponseContent? result = serializer.Deserialize(json); + + // Assert + TextField? resultField = result?.Sitecore?.Route? + .Placeholders["headless-header"].ComponentAt(0)? + .Placeholders["sxa-header"].ComponentAt(0)? + .Fields["Text"] + .Read(); + + dynamic? expectedField = jsonModel.sitecore.route + .placeholders["headless-header"][0] + .placeholders["sxa-header"][0] + .fields.Text; + + resultField!.Value.Should().Be(expectedField.value.Value); + } } \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Integration.Tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Integration.Tests.csproj b/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Integration.Tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Integration.Tests.csproj index 54bab10..e93e15f 100644 --- a/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Integration.Tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Integration.Tests.csproj +++ b/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Integration.Tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Integration.Tests.csproj @@ -17,6 +17,10 @@ Json\edit-in-horizon-mode.json Always + + Json\headlessSxa.json + Always + diff --git a/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Serialization/Converter/FieldParserTests.cs b/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Serialization/Converter/FieldParserTests.cs index d552a58..191a98a 100644 --- a/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Serialization/Converter/FieldParserTests.cs +++ b/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Serialization/Converter/FieldParserTests.cs @@ -27,23 +27,22 @@ public class FieldParserTests }; [Fact] - public void ParseFields_IncorrectJsonNotObject_ShouldThrowJsonException() + public void ParseFields_JsonNotObject_ShouldWrapAsCustomContent() { // Arrange - void Read() - { - const string json = "[]"; - byte[] bytes = [.. Encoding.UTF8.GetBytes(json)]; - Utf8JsonReader reader = new(bytes); - reader.Read(); - _sut.ParseFields(ref reader); - } + const string json = "[]"; + byte[] bytes = [.. Encoding.UTF8.GetBytes(json)]; + Utf8JsonReader reader = new(bytes); + reader.Read(); // Act - Action result = Read; + Dictionary result = _sut.ParseFields(ref reader); // Assert - result.Should().Throw(); + result.Should().ContainSingle(); + (string key, IFieldReader value) = result.First(); + key.Should().Be(FieldParser.CustomContentFieldKey); + value.Should().BeOfType(); } [Fact] diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/ComponentModels/CustomResolver.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/ComponentModels/CustomResolver.cs new file mode 100644 index 0000000..1cc7966 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/ComponentModels/CustomResolver.cs @@ -0,0 +1,10 @@ +using Sitecore.AspNetCore.SDK.LayoutService.Client.Serialization.Converter; +using Sitecore.AspNetCore.SDK.RenderingEngine.Binding.Attributes; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests.ComponentModels; + +public class CustomResolver +{ + [SitecoreComponentField(Name = FieldParser.CustomContentFieldKey)] + public CustomResolverModel[]? CustomContent { get; set; } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/ComponentModels/CustomResolverModel.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/ComponentModels/CustomResolverModel.cs new file mode 100644 index 0000000..6443b64 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/ComponentModels/CustomResolverModel.cs @@ -0,0 +1,17 @@ +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model.Fields; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests.ComponentModels +{ + public class CustomResolverModel + { + public List? Styles { get; set; } = []; + + public List? Children { get; set; } = []; + + public string? Href { get; set; } + + public string? Querystring { get; set; } + + public TextField? NavigationTitle { get; set; } + } +} diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/ComponentModels/PartialDesignDynamicPlaceholder.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/ComponentModels/PartialDesignDynamicPlaceholder.cs new file mode 100644 index 0000000..4fc10ad --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/ComponentModels/PartialDesignDynamicPlaceholder.cs @@ -0,0 +1,9 @@ +using Sitecore.AspNetCore.SDK.RenderingEngine.Binding.Attributes; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests.ComponentModels; + +public class PartialDesignDynamicPlaceholder +{ + [SitecoreComponentParameter(Name ="sig")] + public string? Sig { get; set; } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Controllers/HomeController.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Controllers/HomeController.cs index 37103ce..a7ec068 100644 --- a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Controllers/HomeController.cs +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Controllers/HomeController.cs @@ -14,7 +14,8 @@ public IActionResult Index(Route route) { TestConstants.NestedPlaceholderPageLayoutId => "NestedPlaceholderPageLayout", TestConstants.VisitorIdentificationPageLayoutId => "VisitorIdentificationLayout", - _ => nameof(Index), + TestConstants.HeadlessSxaLayoutId => "HeadlessSxaLayout", + _ => nameof(Index) }; return View(view); diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/Binding/CustomResolverBindingFixture.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/Binding/CustomResolverBindingFixture.cs new file mode 100644 index 0000000..ffc4c13 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/Binding/CustomResolverBindingFixture.cs @@ -0,0 +1,86 @@ +using System.Net; +using FluentAssertions; +using HtmlAgilityPack; +using Microsoft.AspNetCore.TestHost; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Extensions; +using Sitecore.AspNetCore.SDK.RenderingEngine.Extensions; +using Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests.ComponentModels; +using Xunit; + +// ReSharper disable StringLiteralTypo +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests.Fixtures.Binding; + +public class CustomResolverBindingFixture : IDisposable +{ + private readonly TestServer _server; + private readonly HttpLayoutClientMessageHandler _mockClientHandler; + private readonly Uri _layoutServiceUri = new("http://layout.service"); + + public CustomResolverBindingFixture() + { + TestServerBuilder testHostBuilder = new(); + _mockClientHandler = new HttpLayoutClientMessageHandler(); + testHostBuilder + .ConfigureServices(builder => + { + builder + .AddSitecoreLayoutService() + .AddHttpHandler("mock", _ => new HttpClient(_mockClientHandler) { BaseAddress = _layoutServiceUri }) + .AsDefaultHandler(); + builder.AddSitecoreRenderingEngine(options => + { + options + .AddModelBoundView("PartialDesignDynamicPlaceholder") + .AddModelBoundView(name => name.Equals("Navigation", StringComparison.OrdinalIgnoreCase), "CustomResolver") + .AddDefaultComponentRenderer(); + }); + }) + .Configure(app => + { + app.UseRouting(); + app.UseSitecoreRenderingEngine(); + app.UseEndpoints(endpoints => + { + endpoints.MapDefaultControllerRoute(); + }); + }); + + _server = testHostBuilder.BuildServer(new Uri("http://localhost")); + } + + [Fact] + public async Task SitecoreLayoutModelBinders_BindDataCorrectly() + { + // Arrange + string json = await File.ReadAllTextAsync("./Json/headlessSxa.json"); + _mockClientHandler.Responses.Push(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent(json) + }); + + HttpClient client = _server.CreateClient(); + + // Act + string response = await client.GetStringAsync(new Uri("/", UriKind.Relative)); + + HtmlDocument doc = new(); + doc.LoadHtml(response); + HtmlNode? sectionNode = doc.DocumentNode.ChildNodes["head"].ChildNodes.First(n => n.HasClass("custom-resolver")); + + // Assert + sectionNode.ChildNodes["ul"].ChildNodes.Count(c => c.Name.Equals("li")).Should().Be(1); + sectionNode.ChildNodes["ul"].ChildNodes["li"].ChildNodes.Count(c => c.Name.Equals("span")).Should().Be(1); + sectionNode.ChildNodes["ul"].ChildNodes["li"].ChildNodes["span"].InnerText.Should().Be("Home"); + + sectionNode.ChildNodes["ul"].ChildNodes["li"].ChildNodes["ul"].ChildNodes.Count(c => c.Name.Equals("li")).Should().Be(1); + sectionNode.ChildNodes["ul"].ChildNodes["li"].ChildNodes["ul"].ChildNodes["li"].ChildNodes["span"].InnerText.Should().Be("About"); + } + + public void Dispose() + { + _server.Dispose(); + _mockClientHandler.Dispose(); + GC.SuppressFinalize(this); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests.csproj b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests.csproj index 6b19193..e710a16 100644 --- a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests.csproj +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests.csproj @@ -18,4 +18,11 @@ + + + Json\headlessSxa.json + Always + + + diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/Shared/Components/SitecoreComponent/CustomResolver.cshtml b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/Shared/Components/SitecoreComponent/CustomResolver.cshtml new file mode 100644 index 0000000..153879a --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/Shared/Components/SitecoreComponent/CustomResolver.cshtml @@ -0,0 +1,24 @@ +@using Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests.ComponentModels +@model Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests.ComponentModels.CustomResolver + +
+
    + @foreach (CustomResolverModel item in Model.CustomContent ?? []) + { +
  • + @item.NavigationTitle?.Value + @if (item.Children?.Count > 0) + { +
      + @foreach (CustomResolverModel subItem in item.Children) + { +
    • + @subItem.NavigationTitle?.Value +
    • + } +
    + } +
  • + } +
+
\ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/Shared/Components/SitecoreComponent/PartialDesignDynamicPlaceholder.cshtml b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/Shared/Components/SitecoreComponent/PartialDesignDynamicPlaceholder.cshtml new file mode 100644 index 0000000..abead79 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/Shared/Components/SitecoreComponent/PartialDesignDynamicPlaceholder.cshtml @@ -0,0 +1,4 @@ +@using Sitecore.AspNetCore.SDK.RenderingEngine.TagHelpers +@model Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests.ComponentModels.PartialDesignDynamicPlaceholder + + \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/Shared/HeadlessSxaLayout.cshtml b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/Shared/HeadlessSxaLayout.cshtml new file mode 100644 index 0000000..9b881d1 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/Shared/HeadlessSxaLayout.cshtml @@ -0,0 +1,12 @@ +

Headless Sxa Layout

+ + + + +
+ +
+ +
+ +
\ No newline at end of file diff --git a/tests/data/Sitecore.AspNetCore.SDK.TestData/TestConstants.cs b/tests/data/Sitecore.AspNetCore.SDK.TestData/TestConstants.cs index cd5a2ec..dd596c0 100644 --- a/tests/data/Sitecore.AspNetCore.SDK.TestData/TestConstants.cs +++ b/tests/data/Sitecore.AspNetCore.SDK.TestData/TestConstants.cs @@ -75,6 +75,8 @@ public static class TestConstants public const string TestParamNameValue = "ParamName-Value"; + public const string HeadlessSxaLayoutId = "96e5f4ba-a2cf-4a4c-a4e7-64da88226362"; + #pragma warning disable SA1401 #pragma warning disable CA2211 public static readonly string TestMultilineFieldValue = $"This is {Environment.NewLine} multiline text"; diff --git a/tests/data/json/headlessSxa.json b/tests/data/json/headlessSxa.json new file mode 100644 index 0000000..65da442 --- /dev/null +++ b/tests/data/json/headlessSxa.json @@ -0,0 +1,334 @@ +{ + "sitecore": { + "context": { + "pageEditing": false, + "site": { + "name": "iva-test" + }, + "pageState": "normal", + "editMode": "chromes", + "language": "en", + "itemPath": "/" + }, + "route": { + "name": "Home", + "displayName": "Home", + "fields": { + "Title": { + "value": "Home" + }, + "Content": { + "value": "" + }, + "NavigationClass": null, + "NavigationTitle": { + "value": "Home" + }, + "NavigationFilter": [], + "SxaTags": [], + "Page Design": null + }, + "databaseName": "master", + "deviceId": "fe5d7fdf-89c0-4d99-9aa3-b5fbd009c9f3", + "itemId": "eb77f03b-ff99-441f-a54d-e0705a42110c", + "itemLanguage": "en", + "itemVersion": 1, + "layoutId": "96e5f4ba-a2cf-4a4c-a4e7-64da88226362", + "templateId": "e9601da9-d186-4ced-83f3-d72c05ec21b4", + "templateName": "Page", + "placeholders": { + "headless-header": [ + { + "uid": "201e78eb-f292-4914-a19f-c11d54ddb727", + "componentName": "PartialDesignDynamicPlaceholder", + "dataSource": "", + "params": { + "sid": "{201E78EB-F292-4914-A19F-C11D54DDB727}", + "ph": "headless-header", + "sig": "sxa-header" + }, + "placeholders": { + "sxa-header": [ + { + "uid": "f6789ac1-de59-44bb-bc4a-7165304b4a83", + "componentName": "RichText", + "dataSource": "/sitecore/content/Test Collection/iva-test/Presentation/Partial Designs/Header/Data/Title", + "params": { + "Styles": "bs-title", + "FieldNames": "Default", + "GridParameters": "col-12 col-lg-6" + }, + "fields": { + "Text": { + "value": "

Skate Park

" + } + } + }, + { + "uid": "bc41022c-5bab-4296-96f5-8b6fd37e9532", + "componentName": "Navigation", + "dataSource": "", + "params": { + "LevelFrom": "{1BB88840-5FB3-4353-AD8D-81136F6FF75A}", + "LevelTo": "{1BB88840-5FB3-4353-AD8D-81136F6FF75A}", + "FieldNames": "Default", + "AddRoot": "1", + "Styles": "position-right navigation-horizontal", + "GridParameters": "col-12", + "NavigationRoot": "{EB77F03B-FF99-441F-A54D-E0705A42110C}" + }, + "fields": [ + { + "Id": "eb77f03b-ff99-441f-a54d-e0705a42110c", + "Styles": [ + "level0", + "submenu", + "item0", + "odd", + "first", + "last", + "active" + ], + "Href": "/", + "Querystring": "", + "NavigationTitle": { + "value": "Home", + "editable": "Home" + }, + "Children": [ + { + "Id": "02120306-a97a-43d7-827f-ea853484fa0a", + "Styles": [ + "level1", + "item0", + "odd", + "first", + "last" + ], + "Href": "/About", + "Querystring": "", + "NavigationTitle": { + "value": "About", + "editable": "About" + } + } + ] + } + ] + } + ] + } + } + ], + "headless-main": [ + { + "uid": "86d9c1c9-1c1a-4886-af9f-526d8033df00", + "componentName": "RichText", + "dataSource": "/sitecore/content/Test Collection/iva-test/Home/Data/Text 1", + "params": { + "Styles": "main-header", + "FieldNames": "Default", + "GridParameters": "col-12 col-sm-12 col-md-12 col-lg-7 col-xl-7 col-xxl-7 offset-0 offset-sm-0 offset-md-0 offset-lg-1 offset-xl-1 offset-xxl-1" + }, + "fields": { + "Text": { + "value": "

Providing wholesale product basics to the best skate brands

" + } + } + }, + { + "uid": "db9b7091-ef01-4ae8-a6ea-a901276dbd36", + "componentName": "Image", + "dataSource": "/sitecore/content/Test Collection/iva-test/Home/Data/Image 1", + "params": { + "DynamicPlaceholderId": "2", + "FieldNames": "Banner", + "GridParameters": "col-12" + }, + "fields": { + "ImageCaption": { + "value": "" + }, + "Image": { + "value": { + "src": "https://xmcloudcm.localhost/-/media/Feature/JSS-Experience-Accelerator/Basic-Site/banner-image.jpg?h=2001&iar=0&w=3000&hash=4909101A3D2487395593CC6E7996A8E2", + "alt": "", + "width": "3000", + "height": "2001" + } + }, + "TargetUrl": { + "value": { + "href": "" + } + } + } + }, + { + "uid": "7b0847fa-3794-4cd2-af82-b22335c5b337", + "componentName": "Container", + "dataSource": "", + "params": { + "Styles": "container", + "GridParameters": "col-12", + "DynamicPlaceholderId": "1" + }, + "placeholders": { + "container-{*}": [ + { + "uid": "55535631-18c1-49c8-8b3d-6345cd2a233b", + "componentName": "RichText", + "dataSource": "/sitecore/content/Test Collection/iva-test/Home/Data/Text 2", + "params": { + "Styles": "main-news-header", + "FieldNames": "Default", + "GridParameters": "col-12 col-lg-7 col-xl-7 col-xxl-7" + }, + "fields": { + "Text": { + "value": "

In the news

\r\n

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam eu turpis molestie, dictum est a, mattis tellus. Sed dignissim, metus nec fringilla accumsan, risus sem sollicitudin lacus, ut interdum tellus elit sed risus. Maecenas eget condimentum velit, sit amet feugiat lectus. 

" + } + } + }, + { + "uid": "968c056d-12ea-4fec-b6bb-e8591db8da53", + "componentName": "Promo", + "dataSource": "/sitecore/content/Test Collection/iva-test/Home/Data/PromoLeft", + "params": { + "Styles": "main-promo-no-border", + "FieldNames": "Default", + "GridParameters": "col-12 col-lg-6 col-xl-6 col-xxl-6" + }, + "fields": { + "PromoIcon2": { + "value": {} + }, + "PromoText3": { + "value": "" + }, + "PromoText": { + "value": "

Latest Streetwear

\r\n

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam eu turpis molestie, dictum est a, mattis tellus.

" + }, + "PromoLink": { + "value": { + "href": "" + } + }, + "PromoIcon": { + "value": { + "src": "https://xmcloudcm.localhost/-/media/Feature/JSS-Experience-Accelerator/Basic-Site/bottom-left.jpg?h=386&iar=0&w=580&hash=1A24C76ABC73C2CA57E40DEFC159F4D3", + "alt": "", + "width": "580", + "height": "386" + } + }, + "PromoText2": { + "value": "" + } + } + }, + { + "uid": "e0f25e4a-e00b-45c9-b863-1c6f063de842", + "componentName": "Promo", + "dataSource": "/sitecore/content/Test Collection/iva-test/Home/Data/PromoRight", + "params": { + "Styles": "main-promo-no-border", + "FieldNames": "Default", + "GridParameters": "col-12 col-lg-6 col-xl-6 col-xxl-6" + }, + "fields": { + "PromoIcon2": { + "value": {} + }, + "PromoText3": { + "value": "" + }, + "PromoText": { + "value": "

Latest Products

\r\n

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam eu turpis molestie, dictum est a, mattis tellus.

" + }, + "PromoLink": { + "value": { + "href": "" + } + }, + "PromoIcon": { + "value": { + "src": "https://xmcloudcm.localhost/-/media/Feature/JSS-Experience-Accelerator/Basic-Site/bottom-right.jpg?h=386&iar=0&w=580&hash=ABE451D7AA461213471A67A9E4FA441B", + "alt": "", + "width": "580", + "height": "386" + } + }, + "PromoText2": { + "value": "" + } + } + } + ] + } + } + ], + "headless-footer": [ + { + "uid": "99a031b4-b77c-4773-bc6e-e60df29abe3a", + "componentName": "PartialDesignDynamicPlaceholder", + "dataSource": "", + "params": { + "sid": "{99A031B4-B77C-4773-BC6E-E60DF29ABE3A}", + "ph": "headless-footer", + "sig": "sxa-footer" + }, + "placeholders": { + "sxa-footer": [ + { + "uid": "08405428-e9fd-42c6-bd69-5cc4e4b682da", + "componentName": "Container", + "dataSource": "", + "params": { + "Styles": "indent-inner container-dark-background ", + "GridParameters": "col-12", + "DynamicPlaceholderId": "1" + }, + "placeholders": { + "container-{*}": [ + { + "uid": "9f05a6cb-a72f-465e-8d85-30f083133eee", + "componentName": "RichText", + "dataSource": "/sitecore/content/Test Collection/iva-test/Presentation/Partial Designs/Footer/Data/Address", + "params": { + "Styles": "contacts", + "FieldNames": "Default", + "GridParameters": "col-12 col-lg-2" + }, + "fields": { + "Text": { + "value": "\r\n

Skate Park

\r\n

12 Forever Street

\r\n

A99 B44

\r\n

The Nation of Skate

\r\n

Made by Sitecore

\r\n

" + } + } + }, + { + "uid": "19ee64e8-099b-4fee-b48f-315d1fac0a40", + "componentName": "RichText", + "dataSource": "/sitecore/content/Test Collection/iva-test/Presentation/Partial Designs/Footer/Data/Contact info", + "params": { + "Styles": "contacts", + "FieldNames": "Default", + "GridParameters": "col-12 col-lg-2" + }, + "fields": { + "Text": { + "value": "\r\n

info@Skatepark.com

\r\n

234-234-234-234

\r\n

\r\n
" + } + } + } + ] + } + } + ] + } + } + ] + } + } + } +} \ No newline at end of file