From 08bd0a489a60702c3123d1db5345f1534473cf98 Mon Sep 17 00:00:00 2001 From: Egil Hansen Date: Sat, 27 Jan 2024 23:21:03 +0000 Subject: [PATCH] fix: change time based config properties to use timespan, add tests --- .github/workflows/ci.yml | 190 ++++++++---------- Htmxor.sln | 18 +- src/Htmxor/Configuration/HtmxConfig.cs | 25 +-- .../Configuration/HtmxConfigHeadOutlet.cs | 16 +- src/Htmxor/Configuration/ScrollBehavior.cs | 9 + .../HtmxConfigJsonSerializerContext.cs | 12 ++ .../JsonCamelCaseStringEnumConverter.cs | 12 ++ .../TimespanMillisecondJsonConverter.cs | 22 ++ src/Htmxor/Configuration/SwapStyle.cs | 7 +- test/Htmxor.TestApp/Components/App.razor | 18 ++ .../Components/Layout/MainLayout.razor | 3 + .../Components/Layout/MainLayout.razor.css | 18 ++ .../Components/Pages/Error.razor | 36 ++++ .../Components/Pages/Home.razor | 7 + test/Htmxor.TestApp/Components/Routes.razor | 6 + test/Htmxor.TestApp/Components/_Imports.razor | 10 + test/Htmxor.TestApp/Htmxor.TestApp.csproj | 17 ++ .../HxRoutableComponents/Test.razor | 2 + test/Htmxor.TestApp/Program.cs | 27 +++ .../Properties/launchSettings.json | 38 ++++ .../appsettings.Development.json | 8 + test/Htmxor.TestApp/appsettings.json | 9 + test/Htmxor.TestApp/wwwroot/app.css | 29 +++ .../Configuration/HtmxConfigHeadOutletTest.cs | 92 +++++++++ .../JsonStringSemanticAssertionsExtensions.cs | 20 ++ test/Htmxor.Tests/Htmxor.Tests.csproj | 39 ++++ test/Htmxor.Tests/_Usings.cs | 3 + 27 files changed, 574 insertions(+), 119 deletions(-) create mode 100644 src/Htmxor/Configuration/Serialization/HtmxConfigJsonSerializerContext.cs create mode 100644 src/Htmxor/Configuration/Serialization/JsonCamelCaseStringEnumConverter.cs create mode 100644 src/Htmxor/Configuration/Serialization/TimespanMillisecondJsonConverter.cs create mode 100644 test/Htmxor.TestApp/Components/App.razor create mode 100644 test/Htmxor.TestApp/Components/Layout/MainLayout.razor create mode 100644 test/Htmxor.TestApp/Components/Layout/MainLayout.razor.css create mode 100644 test/Htmxor.TestApp/Components/Pages/Error.razor create mode 100644 test/Htmxor.TestApp/Components/Pages/Home.razor create mode 100644 test/Htmxor.TestApp/Components/Routes.razor create mode 100644 test/Htmxor.TestApp/Components/_Imports.razor create mode 100644 test/Htmxor.TestApp/Htmxor.TestApp.csproj create mode 100644 test/Htmxor.TestApp/HxRoutableComponents/Test.razor create mode 100644 test/Htmxor.TestApp/Program.cs create mode 100644 test/Htmxor.TestApp/Properties/launchSettings.json create mode 100644 test/Htmxor.TestApp/appsettings.Development.json create mode 100644 test/Htmxor.TestApp/appsettings.json create mode 100644 test/Htmxor.TestApp/wwwroot/app.css create mode 100644 test/Htmxor.Tests/Configuration/HtmxConfigHeadOutletTest.cs create mode 100644 test/Htmxor.Tests/FluentAssertions/JsonStringSemanticAssertionsExtensions.cs create mode 100644 test/Htmxor.Tests/Htmxor.Tests.csproj create mode 100644 test/Htmxor.Tests/_Usings.cs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b67d851..034bd2d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,10 +36,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v3 with: - dotnet-version: | - 3.1.x - 6.0.x - 8.0.x + dotnet-version: 8.0.x # Create the NuGet package in the folder from the environment variable NuGetDirectory - run: dotnet pack --configuration Release --output ${{ env.NuGetDirectory }} @@ -75,98 +72,92 @@ jobs: shell: pwsh run: meziantou.validate-nuget-package (Get-ChildItem "${{ env.NuGetDirectory }}/*.nupkg") --excluded-rules IconMustBeSet - # run-test: - # runs-on: ubuntu-latest - # timeout-minutes: 30 - # strategy: - # matrix: - # framework: [ net8.0 ] - # fail-fast: false - # env: - # TestResultsDirectory: ${{ github.workspace }}/TestResults - # permissions: - # checks: write - # steps: - # - uses: actions/checkout@v3 - - # - name: Setup .NET - # uses: actions/setup-dotnet@v3 - # with: - # dotnet-version: | - # 3.1.x - # 6.0.x - # 8.0.x - - # - name: Run tests - # run: dotnet test --configuration Release --framework ${{ matrix.framework }} --logger trx --results-directory "${{ env.TestResultsDirectory }}" --collect:"XPlat Code Coverage" --blame-hang --blame-hang-timeout 5min - - # - uses: actions/upload-artifact@v3 - # if: always() - # with: - # name: test-results-${{ matrix.framework }} - # if-no-files-found: error - # retention-days: 3 - # path: ${{ env.TestResultsDirectory }}/**/* - - # - name: Test Report - # uses: dorny/test-reporter@v1 - # if: github.actor != 'dependabot[bot]' && (success() || failure()) && github.repository_owner == 'egil' - # with: - # name: test-results-${{ matrix.framework }} - # path: ${{ env.TestResultsDirectory }}/**/*.trx - # path-replace-backslashes: 'true' - # reporter: dotnet-trx - - # run-stryker: - # runs-on: ubuntu-latest - # if: github.event_name != 'release' - # env: - # StrykerDirectory: ${{ github.workspace }}/Stryker - # permissions: - # statuses: write - # steps: - # - uses: actions/checkout@v3 - - # - name: Setup .NET - # uses: actions/setup-dotnet@v3 - # with: - # dotnet-version: | - # 3.1.x - # 6.0.x - # 8.0.x - - # - name: Install Stryker.NET - # run: dotnet tool install -g dotnet-stryker - - # - name: Run Stryker.NET - # id: stryker - # run: | - # cd test/TimeProviderExtensions.Tests - # dotnet stryker --config-file "../../stryker-config.json" --dashboard-api-key "${{ secrets.STRYKER_DASHBOARD_API_KEY }}" --version ${{ env.BRANCH_NAME }} --output ${{ env.StrykerDirectory }} - - # - run: | - # cat ${{ env.StrykerDirectory }}/reports/mutation-report.md >> $GITHUB_STEP_SUMMARY - # echo "" >> $GITHUB_STEP_SUMMARY - # echo "View the [full report](https://dashboard.stryker-mutator.io/reports/github.com/egil/TimeProviderExtensions/${{ env.BRANCH_NAME }})." >> $GITHUB_STEP_SUMMARY - - # - name: Stryker Report - # if: github.actor != 'dependabot[bot]' && (success() || failure()) && github.repository_owner == 'egil' - # uses: Sibz/github-status-action@v1 - # with: - # authToken: ${{secrets.GITHUB_TOKEN}} - # context: stryker-report" - # description: "See report" - # state: ${{ steps.stryker.conclusion }} - # sha: ${{ github.event.pull_request.head.sha || github.sha }} - # target_url: https://dashboard.stryker-mutator.io/reports/github.com/egil/TimeProviderExtensions/${{ env.BRANCH_NAME }} - - # - uses: actions/upload-artifact@v3 - # if: steps.stryker.conclusion == 'success' || steps.stryker.conclusion == 'failure' - # with: - # name: stryker-reports - # if-no-files-found: error - # retention-days: 3 - # path: ${{ env.StrykerDirectory }}/**/* + run-test: + runs-on: ubuntu-latest + timeout-minutes: 30 + strategy: + matrix: + framework: [ net8.0 ] + fail-fast: false + env: + TestResultsDirectory: ${{ github.workspace }}/TestResults + permissions: + checks: write + steps: + - uses: actions/checkout@v3 + + - name: Setup .NET + uses: actions/setup-dotnet@v3 + with: + dotnet-version: 8.0.x + + - name: Run tests + run: dotnet test --configuration Release --framework ${{ matrix.framework }} --logger trx --results-directory "${{ env.TestResultsDirectory }}" --collect:"XPlat Code Coverage" --blame-hang --blame-hang-timeout 5min + + - uses: actions/upload-artifact@v3 + if: always() + with: + name: test-results-${{ matrix.framework }} + if-no-files-found: error + retention-days: 3 + path: ${{ env.TestResultsDirectory }}/**/* + + - name: Test Report + uses: dorny/test-reporter@v1 + if: github.actor != 'dependabot[bot]' && (success() || failure()) && github.repository_owner == 'egil' + with: + name: test-results-${{ matrix.framework }} + path: ${{ env.TestResultsDirectory }}/**/*.trx + path-replace-backslashes: 'true' + reporter: dotnet-trx + + run-stryker: + runs-on: ubuntu-latest + if: github.event_name != 'release' + env: + StrykerDirectory: ${{ github.workspace }}/Stryker + permissions: + statuses: write + steps: + - uses: actions/checkout@v3 + + - name: Setup .NET + uses: actions/setup-dotnet@v3 + with: + dotnet-version: 8.0.x + + - name: Install Stryker.NET + run: dotnet tool install -g dotnet-stryker + + - name: Run Stryker.NET + id: stryker + run: | + cd test/TimeProviderExtensions.Tests + dotnet stryker --config-file "../../stryker-config.json" --dashboard-api-key "${{ secrets.STRYKER_DASHBOARD_API_KEY }}" --version ${{ env.BRANCH_NAME }} --output ${{ env.StrykerDirectory }} + + - run: | + cat ${{ env.StrykerDirectory }}/reports/mutation-report.md >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "View the [full report](https://dashboard.stryker-mutator.io/reports/github.com/egil/TimeProviderExtensions/${{ env.BRANCH_NAME }})." >> $GITHUB_STEP_SUMMARY + + - name: Stryker Report + if: github.actor != 'dependabot[bot]' && (success() || failure()) && github.repository_owner == 'egil' + uses: Sibz/github-status-action@v1 + with: + authToken: ${{secrets.GITHUB_TOKEN}} + context: stryker-report" + description: "See report" + state: ${{ steps.stryker.conclusion }} + sha: ${{ github.event.pull_request.head.sha || github.sha }} + target_url: https://dashboard.stryker-mutator.io/reports/github.com/egil/TimeProviderExtensions/${{ env.BRANCH_NAME }} + + - uses: actions/upload-artifact@v3 + if: steps.stryker.conclusion == 'success' || steps.stryker.conclusion == 'failure' + with: + name: stryker-reports + if-no-files-found: error + retention-days: 3 + path: ${{ env.StrykerDirectory }}/**/* dependency-review: runs-on: ubuntu-latest @@ -192,10 +183,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v3 with: - dotnet-version: | - 3.1.x - 6.0.x - 8.0.x + dotnet-version: 8.0.x - run: dotnet build --configuration Release @@ -231,7 +219,7 @@ jobs: # You can update this logic if you want to manage releases differently if: github.event_name == 'release' runs-on: ubuntu-latest - needs: [ validate-nuget ] + needs: [ validate-nuget, run-test ] steps: - uses: actions/download-artifact@v3 with: diff --git a/Htmxor.sln b/Htmxor.sln index 71b4bbb..2379601 100644 --- a/Htmxor.sln +++ b/Htmxor.sln @@ -18,7 +18,13 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HtmxBlazorSSR", "samples\Ht EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{A50FFB42-310B-45BC-BDDF-ADE3B61D61EB}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Htmxor", "src\Htmxor\Htmxor.csproj", "{8E83BAAD-08DC-4CAC-AFE7-5E82D5FCAF41}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Htmxor", "src\Htmxor\Htmxor.csproj", "{8E83BAAD-08DC-4CAC-AFE7-5E82D5FCAF41}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{BB63193F-EEC0-4EE6-B89E-9B4E1983E2FF}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Htmxor.Tests", "test\Htmxor.Tests\Htmxor.Tests.csproj", "{DBF3F19B-27E8-4971-BDCF-ECC021D55BFA}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Htmxor.TestApp", "test\Htmxor.TestApp\Htmxor.TestApp.csproj", "{0347D883-0078-4F68-BA24-DA7E3004D31D}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -38,6 +44,14 @@ Global {8E83BAAD-08DC-4CAC-AFE7-5E82D5FCAF41}.Debug|Any CPU.Build.0 = Debug|Any CPU {8E83BAAD-08DC-4CAC-AFE7-5E82D5FCAF41}.Release|Any CPU.ActiveCfg = Release|Any CPU {8E83BAAD-08DC-4CAC-AFE7-5E82D5FCAF41}.Release|Any CPU.Build.0 = Release|Any CPU + {DBF3F19B-27E8-4971-BDCF-ECC021D55BFA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DBF3F19B-27E8-4971-BDCF-ECC021D55BFA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DBF3F19B-27E8-4971-BDCF-ECC021D55BFA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DBF3F19B-27E8-4971-BDCF-ECC021D55BFA}.Release|Any CPU.Build.0 = Release|Any CPU + {0347D883-0078-4F68-BA24-DA7E3004D31D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0347D883-0078-4F68-BA24-DA7E3004D31D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0347D883-0078-4F68-BA24-DA7E3004D31D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0347D883-0078-4F68-BA24-DA7E3004D31D}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -46,6 +60,8 @@ Global {7058400A-CF52-44D4-8AB1-A5D3B1248E09} = {8D4A2D7D-532C-4B68-B5D0-17873D49FB0D} {AF1A487F-CE27-48EB-8390-04C5EC1F34F8} = {8D4A2D7D-532C-4B68-B5D0-17873D49FB0D} {8E83BAAD-08DC-4CAC-AFE7-5E82D5FCAF41} = {A50FFB42-310B-45BC-BDDF-ADE3B61D61EB} + {DBF3F19B-27E8-4971-BDCF-ECC021D55BFA} = {BB63193F-EEC0-4EE6-B89E-9B4E1983E2FF} + {0347D883-0078-4F68-BA24-DA7E3004D31D} = {BB63193F-EEC0-4EE6-B89E-9B4E1983E2FF} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {430F7B4E-864E-44B7-84A0-A903E7386E3B} diff --git a/src/Htmxor/Configuration/HtmxConfig.cs b/src/Htmxor/Configuration/HtmxConfig.cs index 818f9b4..cdc97d0 100644 --- a/src/Htmxor/Configuration/HtmxConfig.cs +++ b/src/Htmxor/Configuration/HtmxConfig.cs @@ -1,6 +1,8 @@ using System.Text.Json; using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; using Htmxor.Antiforgery; +using Htmxor.Configuration.Serialization; namespace Htmxor.Configuration; @@ -9,15 +11,10 @@ namespace Htmxor.Configuration; /// public record class HtmxConfig { - public readonly static JsonSerializerOptions SerializerOptions = new() - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, - Converters = - { - new JsonStringEnumConverter(JsonNamingPolicy.CamelCase, allowIntegerValues: false), - }, - }; + /// + /// Default used with . + /// + public readonly static JsonTypeInfo JsonTypeInfo = HtmxConfigJsonSerializerContext.Default.HtmxConfig; /// /// Defaults to if this property is null. really only useful for testing @@ -48,13 +45,13 @@ public record class HtmxConfig /// Defaults to if this property is null. /// [JsonPropertyName("defaultSwapDelay")] - public int? DefaultSwapDelay { get; set; } + public TimeSpan? DefaultSwapDelay { get; set; } /// /// Defaults to if this property is null. /// [JsonPropertyName("defaultSettleDelay")] - public int? DefaultSettleDelay { get; set; } + public TimeSpan? DefaultSettleDelay { get; set; } /// /// Defaults to if this property is null. @@ -160,7 +157,7 @@ public record class HtmxConfig /// The number of milliseconds a request can take before automatically being terminated /// [JsonPropertyName("timeout")] - public int? Timeout { get; set; } + public TimeSpan? Timeout { get; set; } /// /// Defaults to if this property is null. @@ -203,7 +200,7 @@ public record class HtmxConfig /// If set to will only allow AJAX requests to the same domain as the current document. /// [JsonPropertyName("selfRequestsOnly")] - public bool? SelfRequestsOnly { get; set; } + public bool SelfRequestsOnly { get; set; } = true; /// /// Defaults to if this property is null. @@ -220,6 +217,6 @@ public record class HtmxConfig [JsonPropertyName("scrollIntoViewOnBoost")] public bool? ScrollIntoViewOnBoost { get; set; } - [JsonInclude] + [JsonInclude, JsonPropertyName("antiforgery")] internal HtmxAntiforgeryOptions? Antiforgery { get; init; } } diff --git a/src/Htmxor/Configuration/HtmxConfigHeadOutlet.cs b/src/Htmxor/Configuration/HtmxConfigHeadOutlet.cs index 9b690cd..b639097 100644 --- a/src/Htmxor/Configuration/HtmxConfigHeadOutlet.cs +++ b/src/Htmxor/Configuration/HtmxConfigHeadOutlet.cs @@ -1,20 +1,32 @@ -using Microsoft.AspNetCore.Components; +using Htmxor.Configuration.Serialization; +using Microsoft.AspNetCore.Components; using System.Text.Json; namespace Htmxor.Configuration; +/// +/// This component will render a meta tag with the serialized object, +/// enabling customization of Htmx. +/// +/// +/// Configure the via the +/// +/// method. +/// public class HtmxConfigHeadOutlet : IComponent { [Inject] private HtmxConfig Config { get; set; } = default!; + /// public void Attach(RenderHandle renderHandle) { - var json = JsonSerializer.Serialize(Config, HtmxConfig.SerializerOptions); + var json = JsonSerializer.Serialize(Config, HtmxConfigJsonSerializerContext.Default.HtmxConfig); renderHandle.Render(builder => { builder.AddMarkupContent(0, @$""); }); } + /// public Task SetParametersAsync(ParameterView parameters) => Task.CompletedTask; } diff --git a/src/Htmxor/Configuration/ScrollBehavior.cs b/src/Htmxor/Configuration/ScrollBehavior.cs index 82e6462..5b948c6 100644 --- a/src/Htmxor/Configuration/ScrollBehavior.cs +++ b/src/Htmxor/Configuration/ScrollBehavior.cs @@ -1,8 +1,17 @@ namespace Htmxor.Configuration; +/// +/// The behavior for a boosted link on page transitions. +/// public enum ScrollBehavior { + /// + /// Smooth will smooth-scroll to the top of the page. + /// Smooth, + /// + /// Auto will behave like a vanilla link. + /// Auto, } diff --git a/src/Htmxor/Configuration/Serialization/HtmxConfigJsonSerializerContext.cs b/src/Htmxor/Configuration/Serialization/HtmxConfigJsonSerializerContext.cs new file mode 100644 index 0000000..f7a0ba0 --- /dev/null +++ b/src/Htmxor/Configuration/Serialization/HtmxConfigJsonSerializerContext.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +namespace Htmxor.Configuration.Serialization; + +[JsonSourceGenerationOptions( + PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + Converters = [typeof(JsonCamelCaseStringEnumConverter), typeof(TimespanMillisecondJsonConverter)])] +[JsonSerializable(typeof(HtmxConfig))] +internal sealed partial class HtmxConfigJsonSerializerContext : JsonSerializerContext +{ +} diff --git a/src/Htmxor/Configuration/Serialization/JsonCamelCaseStringEnumConverter.cs b/src/Htmxor/Configuration/Serialization/JsonCamelCaseStringEnumConverter.cs new file mode 100644 index 0000000..6d3c208 --- /dev/null +++ b/src/Htmxor/Configuration/Serialization/JsonCamelCaseStringEnumConverter.cs @@ -0,0 +1,12 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Htmxor.Configuration.Serialization; + +internal sealed class JsonCamelCaseStringEnumConverter : JsonStringEnumConverter +{ + public JsonCamelCaseStringEnumConverter() + : base(JsonNamingPolicy.CamelCase, allowIntegerValues: false) + { + } +} diff --git a/src/Htmxor/Configuration/Serialization/TimespanMillisecondJsonConverter.cs b/src/Htmxor/Configuration/Serialization/TimespanMillisecondJsonConverter.cs new file mode 100644 index 0000000..a882f92 --- /dev/null +++ b/src/Htmxor/Configuration/Serialization/TimespanMillisecondJsonConverter.cs @@ -0,0 +1,22 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Htmxor.Configuration.Serialization; + +internal sealed class TimespanMillisecondJsonConverter : JsonConverter +{ + public override TimeSpan? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + => reader.TokenType switch + { + JsonTokenType.Number => TimeSpan.FromMilliseconds(reader.GetInt32()), + _ => null + }; + + public override void Write(Utf8JsonWriter writer, TimeSpan? value, JsonSerializerOptions options) + { + if (value.HasValue) + { + writer.WriteNumberValue((int)value.Value.TotalMilliseconds); + } + } +} diff --git a/src/Htmxor/Configuration/SwapStyle.cs b/src/Htmxor/Configuration/SwapStyle.cs index 82513bc..d0830da 100644 --- a/src/Htmxor/Configuration/SwapStyle.cs +++ b/src/Htmxor/Configuration/SwapStyle.cs @@ -1,5 +1,10 @@ -namespace Htmxor.Configuration; +using System.Text.Json.Serialization; +namespace Htmxor.Configuration; + +/// +/// How to swap the response into the target element. +/// public enum SwapStyle { /// diff --git a/test/Htmxor.TestApp/Components/App.razor b/test/Htmxor.TestApp/Components/App.razor new file mode 100644 index 0000000..c7e0824 --- /dev/null +++ b/test/Htmxor.TestApp/Components/App.razor @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/test/Htmxor.TestApp/Components/Layout/MainLayout.razor b/test/Htmxor.TestApp/Components/Layout/MainLayout.razor new file mode 100644 index 0000000..e1a9a75 --- /dev/null +++ b/test/Htmxor.TestApp/Components/Layout/MainLayout.razor @@ -0,0 +1,3 @@ +@inherits LayoutComponentBase + +@Body diff --git a/test/Htmxor.TestApp/Components/Layout/MainLayout.razor.css b/test/Htmxor.TestApp/Components/Layout/MainLayout.razor.css new file mode 100644 index 0000000..df8c10f --- /dev/null +++ b/test/Htmxor.TestApp/Components/Layout/MainLayout.razor.css @@ -0,0 +1,18 @@ +#blazor-error-ui { + background: lightyellow; + bottom: 0; + box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); + display: none; + left: 0; + padding: 0.6rem 1.25rem 0.7rem 1.25rem; + position: fixed; + width: 100%; + z-index: 1000; +} + + #blazor-error-ui .dismiss { + cursor: pointer; + position: absolute; + right: 0.75rem; + top: 0.5rem; + } diff --git a/test/Htmxor.TestApp/Components/Pages/Error.razor b/test/Htmxor.TestApp/Components/Pages/Error.razor new file mode 100644 index 0000000..576cc2d --- /dev/null +++ b/test/Htmxor.TestApp/Components/Pages/Error.razor @@ -0,0 +1,36 @@ +@page "/Error" +@using System.Diagnostics + +Error + +

Error.

+

An error occurred while processing your request.

+ +@if (ShowRequestId) +{ +

+ Request ID: @RequestId +

+} + +

Development Mode

+

+ Swapping to Development environment will display more detailed information about the error that occurred. +

+

+ The Development environment shouldn't be enabled for deployed applications. + It can result in displaying sensitive information from exceptions to end users. + For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development + and restarting the app. +

+ +@code{ + [CascadingParameter] + private HttpContext? HttpContext { get; set; } + + private string? RequestId { get; set; } + private bool ShowRequestId => !string.IsNullOrEmpty(RequestId); + + protected override void OnInitialized() => + RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier; +} diff --git a/test/Htmxor.TestApp/Components/Pages/Home.razor b/test/Htmxor.TestApp/Components/Pages/Home.razor new file mode 100644 index 0000000..9001e0b --- /dev/null +++ b/test/Htmxor.TestApp/Components/Pages/Home.razor @@ -0,0 +1,7 @@ +@page "/" + +Home + +

Hello, world!

+ +Welcome to your new app. diff --git a/test/Htmxor.TestApp/Components/Routes.razor b/test/Htmxor.TestApp/Components/Routes.razor new file mode 100644 index 0000000..d0df781 --- /dev/null +++ b/test/Htmxor.TestApp/Components/Routes.razor @@ -0,0 +1,6 @@ + + + + + + diff --git a/test/Htmxor.TestApp/Components/_Imports.razor b/test/Htmxor.TestApp/Components/_Imports.razor new file mode 100644 index 0000000..d8a448e --- /dev/null +++ b/test/Htmxor.TestApp/Components/_Imports.razor @@ -0,0 +1,10 @@ +@using System.Net.Http +@using System.Net.Http.Json +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using static Microsoft.AspNetCore.Components.Web.RenderMode +@using Microsoft.AspNetCore.Components.Web.Virtualization +@using Microsoft.JSInterop +@using Htmxor.TestApp +@using Htmxor.TestApp.Components diff --git a/test/Htmxor.TestApp/Htmxor.TestApp.csproj b/test/Htmxor.TestApp/Htmxor.TestApp.csproj new file mode 100644 index 0000000..9c82437 --- /dev/null +++ b/test/Htmxor.TestApp/Htmxor.TestApp.csproj @@ -0,0 +1,17 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + diff --git a/test/Htmxor.TestApp/HxRoutableComponents/Test.razor b/test/Htmxor.TestApp/HxRoutableComponents/Test.razor new file mode 100644 index 0000000..183564e --- /dev/null +++ b/test/Htmxor.TestApp/HxRoutableComponents/Test.razor @@ -0,0 +1,2 @@ +@attribute [HxRoute("/hx/test")] +

Test

\ No newline at end of file diff --git a/test/Htmxor.TestApp/Program.cs b/test/Htmxor.TestApp/Program.cs new file mode 100644 index 0000000..e0c5ca3 --- /dev/null +++ b/test/Htmxor.TestApp/Program.cs @@ -0,0 +1,27 @@ +using Htmxor; +using Htmxor.TestApp.Components; + +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container. +builder.Services.AddRazorComponents(); +builder.AddHtmx(); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (!app.Environment.IsDevelopment()) +{ + app.UseExceptionHandler("/Error", createScopeForErrors: true); + // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. + app.UseHsts(); +} + +app.UseHttpsRedirection(); + +app.UseStaticFiles(); +app.UseAntiforgery(); + +app.MapRazorComponents(); + +app.Run(); diff --git a/test/Htmxor.TestApp/Properties/launchSettings.json b/test/Htmxor.TestApp/Properties/launchSettings.json new file mode 100644 index 0000000..9dd85d1 --- /dev/null +++ b/test/Htmxor.TestApp/Properties/launchSettings.json @@ -0,0 +1,38 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:64889", + "sslPort": 44347 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:5284", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:7287;http://localhost:5284", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } + } diff --git a/test/Htmxor.TestApp/appsettings.Development.json b/test/Htmxor.TestApp/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/test/Htmxor.TestApp/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/test/Htmxor.TestApp/appsettings.json b/test/Htmxor.TestApp/appsettings.json new file mode 100644 index 0000000..10f68b8 --- /dev/null +++ b/test/Htmxor.TestApp/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/test/Htmxor.TestApp/wwwroot/app.css b/test/Htmxor.TestApp/wwwroot/app.css new file mode 100644 index 0000000..e398853 --- /dev/null +++ b/test/Htmxor.TestApp/wwwroot/app.css @@ -0,0 +1,29 @@ +h1:focus { + outline: none; +} + +.valid.modified:not([type=checkbox]) { + outline: 1px solid #26b050; +} + +.invalid { + outline: 1px solid #e50000; +} + +.validation-message { + color: #e50000; +} + +.blazor-error-boundary { + background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121; + padding: 1rem 1rem 1rem 3.7rem; + color: white; +} + + .blazor-error-boundary::after { + content: "An error has occurred." + } + +.darker-border-checkbox.form-check-input { + border-color: #929292; +} diff --git a/test/Htmxor.Tests/Configuration/HtmxConfigHeadOutletTest.cs b/test/Htmxor.Tests/Configuration/HtmxConfigHeadOutletTest.cs new file mode 100644 index 0000000..5a5aed6 --- /dev/null +++ b/test/Htmxor.Tests/Configuration/HtmxConfigHeadOutletTest.cs @@ -0,0 +1,92 @@ +using Bunit; +using Microsoft.Extensions.DependencyInjection; + +namespace Htmxor.Configuration; + +public class HtmxConfigHeadOutletTest : TestContext +{ + [Fact] + public void HtmxConfig_serializer() + { + var config = new HtmxConfig + { + AddedClass = "added-class", + AllowEval = true, + AllowScriptTags = true, + AttributesToSettle = ["attr1", "attr2"], + DefaultFocusScroll = true, + DefaultSettleDelay = TimeSpan.FromHours(1), + DefaultSwapDelay = TimeSpan.FromMinutes(1), + DefaultSwapStyle = SwapStyle.BeforeBegin, + DisableSelector = "disable-selector", + GetCacheBusterParam = true, + GlobalViewTransitions = true, + HistoryCacheSize = 1234, + HistoryEnabled = true, + IgnoreTitle = true, + IncludeIndicatorStyles = true, + IndicatorClass = "indicator-class", + InlineScriptNonce = "inline-script-nonce", + MethodsThatUseUrlParams = ["GET", "POST", "DELETE"], + RefreshOnHistoryMiss = true, + RequestClass = "request-class", + ScrollBehavior = ScrollBehavior.Smooth, + ScrollIntoViewOnBoost = true, + SelfRequestsOnly = true, + SettlingClass = "settling-class", + SwappingClass = "swapping-class", + Timeout = TimeSpan.FromSeconds(30), + UseTemplateFragments = true, + WithCredentials = true, + WsBinaryType = "ws-binary-type", + WsReconnectDelay = "full-jitter", + }; + Services.AddSingleton(config); + + var cut = RenderComponent(); + + var meta = cut.Find("meta"); + meta.GetAttribute("name").Should().Be("htmx-config"); + meta.GetAttribute("content").Should().BeJsonSemanticallyEqualTo(""" + { + "addedClass": "added-class", + "allowEval": true, + "allowScriptTags": true, + "attributesToSettle": [ + "attr1", + "attr2" + ], + "defaultFocusScroll": true, + "defaultSwapStyle": "beforeBegin", + "defaultSwapDelay": 60000, + "defaultSettleDelay": 3600000, + "disableSelector": "disable-selector", + "getCacheBusterParam": true, + "globalViewTransitions": true, + "historyCacheSize": 1234, + "historyEnabled": true, + "ignoreTitle": true, + "includeIndicatorStyles": true, + "indicatorClass": "indicator-class", + "inlineScriptNonce": "inline-script-nonce", + "methodsThatUseUrlParams": [ + "GET", + "POST", + "DELETE" + ], + "refreshOnHistoryMiss": true, + "requestClass": "request-class", + "scrollBehavior": "smooth", + "scrollIntoViewOnBoost": true, + "selfRequestsOnly": true, + "settlingClass": "settling-class", + "swappingClass": "swapping-class", + "timeout": 30000, + "useTemplateFragments": true, + "withCredentials": true, + "wsBinaryType": "ws-binary-type", + "wsReconnectDelay": "full-jitter" + } + """); + } +} diff --git a/test/Htmxor.Tests/FluentAssertions/JsonStringSemanticAssertionsExtensions.cs b/test/Htmxor.Tests/FluentAssertions/JsonStringSemanticAssertionsExtensions.cs new file mode 100644 index 0000000..5cea30f --- /dev/null +++ b/test/Htmxor.Tests/FluentAssertions/JsonStringSemanticAssertionsExtensions.cs @@ -0,0 +1,20 @@ +using System.Diagnostics.CodeAnalysis; +using System.Text.Json.JsonDiffPatch.Xunit; +using System.Text.Json.Nodes; +using FluentAssertions.Primitives; + +namespace FluentAssertions; + +internal static class JsonStringSemanticAssertionsExtensions +{ + public static AndConstraint BeJsonSemanticallyEqualTo( + this StringAssertions assertions, + [StringSyntax(StringSyntaxAttribute.Json)] string expected) + { + JsonAssert.Equal( + JsonNode.Parse(expected), + JsonNode.Parse(assertions.Subject), + output: true); + return new AndConstraint(assertions); + } +} \ No newline at end of file diff --git a/test/Htmxor.Tests/Htmxor.Tests.csproj b/test/Htmxor.Tests/Htmxor.Tests.csproj new file mode 100644 index 0000000..d00b112 --- /dev/null +++ b/test/Htmxor.Tests/Htmxor.Tests.csproj @@ -0,0 +1,39 @@ + + + + net8.0 + enable + enable + false + true + Htmxor + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + diff --git a/test/Htmxor.Tests/_Usings.cs b/test/Htmxor.Tests/_Usings.cs new file mode 100644 index 0000000..8706b65 --- /dev/null +++ b/test/Htmxor.Tests/_Usings.cs @@ -0,0 +1,3 @@ +global using Alba; +global using FluentAssertions; +global using Xunit; \ No newline at end of file