diff --git a/Directory.Packages.props b/Directory.Packages.props index c6d90e8..4c5b154 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -7,6 +7,7 @@ + diff --git a/src/Code311.Persistence.EFCore/Code311.Persistence.EFCore.csproj b/src/Code311.Persistence.EFCore/Code311.Persistence.EFCore.csproj index 1dab6a4..dab7ebc 100644 --- a/src/Code311.Persistence.EFCore/Code311.Persistence.EFCore.csproj +++ b/src/Code311.Persistence.EFCore/Code311.Persistence.EFCore.csproj @@ -13,6 +13,7 @@ + diff --git a/src/Code311.Tabler.Components/Data/DataComponents.cs b/src/Code311.Tabler.Components/Data/DataComponents.cs index 8b715e5..7042423 100644 --- a/src/Code311.Tabler.Components/Data/DataComponents.cs +++ b/src/Code311.Tabler.Components/Data/DataComponents.cs @@ -3,6 +3,7 @@ using Code311.Ui.Abstractions.Semantics; using Microsoft.AspNetCore.Html; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ViewComponents; using Microsoft.AspNetCore.Razor.TagHelpers; namespace Code311.Tabler.Components.Data; diff --git a/src/Code311.Tabler.Components/Feedback/FeedbackComponents.cs b/src/Code311.Tabler.Components/Feedback/FeedbackComponents.cs index 4ebdb85..dae65b8 100644 --- a/src/Code311.Tabler.Components/Feedback/FeedbackComponents.cs +++ b/src/Code311.Tabler.Components/Feedback/FeedbackComponents.cs @@ -4,6 +4,7 @@ using Code311.Ui.Core.Loading; using Microsoft.AspNetCore.Html; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ViewComponents; using Microsoft.AspNetCore.Razor.TagHelpers; namespace Code311.Tabler.Components.Feedback; diff --git a/src/Code311.Tabler.Components/Forms/FormTagHelpers.cs b/src/Code311.Tabler.Components/Forms/FormTagHelpers.cs index 8ab5a32..af31ee1 100644 --- a/src/Code311.Tabler.Components/Forms/FormTagHelpers.cs +++ b/src/Code311.Tabler.Components/Forms/FormTagHelpers.cs @@ -47,7 +47,7 @@ public override void Process(TagHelperContext context, TagHelperOutput output) } [HtmlTargetElement("cd311-select")] -public sealed class Cd311SelectTagHelper(ITablerSemanticClassMapper mapper) : TagHelper +public sealed class Cd311SelectTagHelper : TagHelper { public string? Field { get; set; } public IReadOnlyCollection? Options { get; set; } diff --git a/src/Code311.Tabler.Components/Forms/FormViewComponents.cs b/src/Code311.Tabler.Components/Forms/FormViewComponents.cs index 9188d8a..bd7668f 100644 --- a/src/Code311.Tabler.Components/Forms/FormViewComponents.cs +++ b/src/Code311.Tabler.Components/Forms/FormViewComponents.cs @@ -3,6 +3,7 @@ using Code311.Ui.Abstractions.Semantics; using Microsoft.AspNetCore.Html; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ViewComponents; namespace Code311.Tabler.Components.Forms; diff --git a/src/Code311.Tabler.Components/Layout/LayoutComponents.cs b/src/Code311.Tabler.Components/Layout/LayoutComponents.cs index ebc5e59..b9fc0a5 100644 --- a/src/Code311.Tabler.Components/Layout/LayoutComponents.cs +++ b/src/Code311.Tabler.Components/Layout/LayoutComponents.cs @@ -2,6 +2,7 @@ using Code311.Ui.Abstractions.Semantics; using Microsoft.AspNetCore.Html; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ViewComponents; using Microsoft.AspNetCore.Razor.TagHelpers; namespace Code311.Tabler.Components.Layout; diff --git a/src/Code311.Tabler.Components/Media/MediaComponents.cs b/src/Code311.Tabler.Components/Media/MediaComponents.cs index 1d5fbc2..672f903 100644 --- a/src/Code311.Tabler.Components/Media/MediaComponents.cs +++ b/src/Code311.Tabler.Components/Media/MediaComponents.cs @@ -2,6 +2,7 @@ using Code311.Ui.Abstractions.Semantics; using Microsoft.AspNetCore.Html; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ViewComponents; using Microsoft.AspNetCore.Razor.TagHelpers; namespace Code311.Tabler.Components.Media; diff --git a/src/Code311.Tabler.Components/Navigation/NavigationComponents.cs b/src/Code311.Tabler.Components/Navigation/NavigationComponents.cs index 8d7f81e..157cdf0 100644 --- a/src/Code311.Tabler.Components/Navigation/NavigationComponents.cs +++ b/src/Code311.Tabler.Components/Navigation/NavigationComponents.cs @@ -3,6 +3,7 @@ using Code311.Ui.Abstractions.Semantics; using Microsoft.AspNetCore.Html; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ViewComponents; using Microsoft.AspNetCore.Razor.TagHelpers; namespace Code311.Tabler.Components.Navigation; diff --git a/src/Code311.Tabler.Dashboard/Kpi/KpiViewComponents.cs b/src/Code311.Tabler.Dashboard/Kpi/KpiViewComponents.cs index 5ca0e07..d3ef221 100644 --- a/src/Code311.Tabler.Dashboard/Kpi/KpiViewComponents.cs +++ b/src/Code311.Tabler.Dashboard/Kpi/KpiViewComponents.cs @@ -2,6 +2,7 @@ using Code311.Tabler.Dashboard.Models; using Microsoft.AspNetCore.Html; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ViewComponents; namespace Code311.Tabler.Dashboard.Kpi; diff --git a/src/Code311.Tabler.Dashboard/Layout/DashboardShellViewComponent.cs b/src/Code311.Tabler.Dashboard/Layout/DashboardShellViewComponent.cs index 1104a68..cf36042 100644 --- a/src/Code311.Tabler.Dashboard/Layout/DashboardShellViewComponent.cs +++ b/src/Code311.Tabler.Dashboard/Layout/DashboardShellViewComponent.cs @@ -3,6 +3,7 @@ using Code311.Ui.Abstractions.Semantics; using Microsoft.AspNetCore.Html; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ViewComponents; namespace Code311.Tabler.Dashboard.Layout; diff --git a/src/Code311.Tabler.Dashboard/Panels/DashboardPanelViewComponents.cs b/src/Code311.Tabler.Dashboard/Panels/DashboardPanelViewComponents.cs index b37c2c2..b7c0ec3 100644 --- a/src/Code311.Tabler.Dashboard/Panels/DashboardPanelViewComponents.cs +++ b/src/Code311.Tabler.Dashboard/Panels/DashboardPanelViewComponents.cs @@ -3,6 +3,7 @@ using Code311.Tabler.Dashboard.Models; using Microsoft.AspNetCore.Html; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ViewComponents; namespace Code311.Tabler.Dashboard.Panels; diff --git a/src/Code311.Tabler.Dashboard/Properties/AssemblyInfo.cs b/src/Code311.Tabler.Dashboard/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..7645eed --- /dev/null +++ b/src/Code311.Tabler.Dashboard/Properties/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Code311.Tests.Tabler.Dashboard")] diff --git a/src/Code311.Tabler.Mvc/DependencyInjection/ServiceCollectionExtensions.cs b/src/Code311.Tabler.Mvc/DependencyInjection/ServiceCollectionExtensions.cs index 8363c55..43d12e2 100644 --- a/src/Code311.Tabler.Mvc/DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/Code311.Tabler.Mvc/DependencyInjection/ServiceCollectionExtensions.cs @@ -58,8 +58,7 @@ public static IServiceCollection AddCode311TablerMvc(this IServiceCollection ser } } -internal sealed class ConfigureCode311MvcOptions( - IServiceProvider serviceProvider) : IConfigureOptions +internal sealed class ConfigureCode311MvcOptions : IConfigureOptions { public void Configure(MvcOptions options) { diff --git a/src/Code311.Tabler.Mvc/Properties/AssemblyInfo.cs b/src/Code311.Tabler.Mvc/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..7acdc69 --- /dev/null +++ b/src/Code311.Tabler.Mvc/Properties/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Code311.Tests.Integration.Mvc")] diff --git a/src/Code311.Tabler.Razor/Properties/AssemblyInfo.cs b/src/Code311.Tabler.Razor/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..cb10dbb --- /dev/null +++ b/src/Code311.Tabler.Razor/Properties/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Code311.Tests.Integration.Razor")] diff --git a/tests/Code311.Tests.Integration.Razor/RazorIntegrationTests.cs b/tests/Code311.Tests.Integration.Razor/RazorIntegrationTests.cs index 260e0f4..5ee63dc 100644 --- a/tests/Code311.Tests.Integration.Razor/RazorIntegrationTests.cs +++ b/tests/Code311.Tests.Integration.Razor/RazorIntegrationTests.cs @@ -10,6 +10,7 @@ using Code311.Ui.Core.Theming; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure; diff --git a/tests/Code311.Tests.Licensing/LicensingServicesTests.cs b/tests/Code311.Tests.Licensing/LicensingServicesTests.cs index f3b0bfd..e5e9c2b 100644 --- a/tests/Code311.Tests.Licensing/LicensingServicesTests.cs +++ b/tests/Code311.Tests.Licensing/LicensingServicesTests.cs @@ -69,7 +69,7 @@ public async Task FeatureGate_ShouldDeny_WhenFeatureMissing() { var services = new ServiceCollection() .AddCode311Licensing(options => options.RequireValidLicenseAtStartup = false) - .AddCode311InMemoryLicenseSource(BuildLicense(features: ["dashboard.basic"])) + .AddCode311InMemoryLicenseSource(BuildLicense(features: new HashSet(StringComparer.OrdinalIgnoreCase) { "dashboard.basic" })) .BuildServiceProvider(); var gate = services.GetRequiredService(); @@ -85,7 +85,7 @@ public async Task FeatureGate_ShouldAllow_WhenFeaturePresent() { var services = new ServiceCollection() .AddCode311Licensing(options => options.RequireValidLicenseAtStartup = false) - .AddCode311InMemoryLicenseSource(BuildLicense(features: ["dashboard.advanced"])) + .AddCode311InMemoryLicenseSource(BuildLicense(features: new HashSet(StringComparer.OrdinalIgnoreCase) { "dashboard.advanced" })) .BuildServiceProvider(); var gate = services.GetRequiredService(); @@ -113,6 +113,7 @@ public void AddCode311Licensing_ShouldRegisterCoreServices() [Fact] public void UiAndTablerPackages_ShouldNotReferenceCode311Licensing() { + var repositoryRoot = FindRepositoryRoot(); var forbiddenProjectFiles = new[] { "src/Code311.Ui.Abstractions/Code311.Ui.Abstractions.csproj", @@ -127,12 +128,30 @@ public void UiAndTablerPackages_ShouldNotReferenceCode311Licensing() foreach (var relativePath in forbiddenProjectFiles) { - var full = Path.Combine(AppContext.BaseDirectory, "../../../../", relativePath); + var full = Path.Combine(repositoryRoot, relativePath); var xml = File.ReadAllText(full); Assert.DoesNotContain("Code311.Licensing", xml, StringComparison.Ordinal); } } + private static string FindRepositoryRoot() + { + var current = new DirectoryInfo(AppContext.BaseDirectory); + + while (current is not null) + { + var solutionPath = Path.Combine(current.FullName, "Code311.sln"); + if (File.Exists(solutionPath)) + { + return current.FullName; + } + + current = current.Parent; + } + + throw new DirectoryNotFoundException("Unable to locate repository root containing Code311.sln."); + } + private static Code311License BuildLicense(DateTimeOffset? expiresUtc = null, DateTimeOffset? notBeforeUtc = null, IReadOnlySet? features = null) => new() { diff --git a/tests/Code311.Tests.Tabler.Components/ComponentRenderingTests.cs b/tests/Code311.Tests.Tabler.Components/ComponentRenderingTests.cs index 9d7401e..8ece790 100644 --- a/tests/Code311.Tests.Tabler.Components/ComponentRenderingTests.cs +++ b/tests/Code311.Tests.Tabler.Components/ComponentRenderingTests.cs @@ -1,4 +1,5 @@ using System.IO; +using System.Text.Encodings.Web; using Code311.Tabler.Components.Common; using Code311.Tabler.Components.Data; using Code311.Tabler.Components.Feedback; @@ -26,7 +27,7 @@ public sealed class ComponentRenderingTests [Fact] public void Cd311Input_ShouldRenderSemanticClasses() { - var helper = new Cd311InputTagHelper(new TablerSemanticClassMapper) + var helper = new Cd311InputTagHelper(new TablerSemanticClassMapper()) { Field = "Email", Label = "Email", @@ -46,7 +47,7 @@ public void Cd311Input_ShouldRenderSemanticClasses() [Fact] public void Cd311Tabs_ShouldRenderItems() { - var helper = new Cd311TabsTagHelper(new TablerSemanticClassMapper) + var helper = new Cd311TabsTagHelper(new TablerSemanticClassMapper()) { Items = [ @@ -66,7 +67,7 @@ public void Cd311Tabs_ShouldRenderItems() [Fact] public void Cd311Card_ShouldSetToneClass() { - var helper = new Cd311CardTagHelper(new TablerSemanticClassMapper) { Tone = UiTone.Warning, Title = "Card" }; + var helper = new Cd311CardTagHelper(new TablerSemanticClassMapper()) { Tone = UiTone.Warning, Title = "Card" }; var output = CreateOutput(); helper.Process(CreateContext(), output); @@ -77,7 +78,7 @@ public void Cd311Card_ShouldSetToneClass() [Fact] public void Cd311Progress_ShouldClampAndRenderBar() { - var helper = new Cd311ProgressTagHelper(new TablerSemanticClassMapper) { Value = 150, Tone = UiTone.Success }; + var helper = new Cd311ProgressTagHelper(new TablerSemanticClassMapper()) { Value = 150, Tone = UiTone.Success }; var output = CreateOutput(); helper.Process(CreateContext(), output); @@ -90,7 +91,7 @@ public void Cd311Progress_ShouldClampAndRenderBar() [Fact] public void Cd311Badge_ShouldRenderTone() { - var helper = new Cd311BadgeTagHelper(new TablerSemanticClassMapper) { Tone = UiTone.Danger, Text = "Blocked" }; + var helper = new Cd311BadgeTagHelper(new TablerSemanticClassMapper()) { Tone = UiTone.Danger, Text = "Blocked" }; var output = CreateOutput(); helper.Process(CreateContext(), output); @@ -102,7 +103,7 @@ public void Cd311Badge_ShouldRenderTone() [Fact] public void Cd311Avatar_ShouldFallbackToInitials() { - var helper = new Cd311AvatarTagHelper(new TablerSemanticClassMapper) { Name = "Code Three" }; + var helper = new Cd311AvatarTagHelper(new TablerSemanticClassMapper()) { Name = "Code Three" }; var output = CreateOutput(); helper.Process(CreateContext(), output); @@ -126,10 +127,12 @@ public void BusyAndAlertComponents_ShouldConsumeUiCoreServices() var host = new Cd311PageAlertHostViewComponent(feedback); Assert.Contains("active", overlayOutput.Attributes["class"].Value?.ToString()); - var result = Assert.IsType(host.Invoke()); + var result = host.Invoke(); + var htmlResult = Assert.IsType(result); using var writer = new StringWriter(); - result.Content.WriteTo(writer, System.Text.Encodings.Web.HtmlEncoder.Default); - Assert.Contains("Saved", writer.ToString()); + htmlResult.EncodedContent.WriteTo(writer, HtmlEncoder.Default); + var html = writer.ToString(); + Assert.Contains("Saved", html); } private static TagHelperContext CreateContext() => diff --git a/tests/Code311.Tests.Tabler.Dashboard/DashboardCompositionTests.cs b/tests/Code311.Tests.Tabler.Dashboard/DashboardCompositionTests.cs index 2aa24c7..b8c4efe 100644 --- a/tests/Code311.Tests.Tabler.Dashboard/DashboardCompositionTests.cs +++ b/tests/Code311.Tests.Tabler.Dashboard/DashboardCompositionTests.cs @@ -1,4 +1,5 @@ using System.IO; +using System.Text.Encodings.Web; using Code311.Tabler.Core.Mapping; using Code311.Tabler.Dashboard.Composition; using Code311.Tabler.Dashboard.Kpi; @@ -41,10 +42,11 @@ [new DashboardPanelModel("p1", "Revenue", "
$100
", UiTone.Success)]) public void DashboardShell_ShouldRenderShellClasses() { var component = new Cd311DashboardShellViewComponent(new TablerSemanticClassMapper()); - var result = Assert.IsType(component.Invoke(new DashboardPageModel("Sales", []), UiLayout.Grid)); + var result = component.Invoke(new DashboardPageModel("Sales", []), UiLayout.Grid); + var htmlResult = Assert.IsType(result); using var writer = new StringWriter(); - result.Content.WriteTo(writer, System.Text.Encodings.Web.HtmlEncoder.Default); + htmlResult.EncodedContent.WriteTo(writer, HtmlEncoder.Default); var html = writer.ToString(); Assert.Contains("cd311-dashboard-shell", html); @@ -55,10 +57,11 @@ public void DashboardShell_ShouldRenderShellClasses() public void KpiSummary_ShouldRenderToneMappedItems() { var component = new Cd311KpiSummaryViewComponent(new TablerSemanticClassMapper()); - var result = Assert.IsType(component.Invoke("KPIs", [new DashboardKpiItem("Users", "100", UiTone.Info, "+5%") ])); + var result = component.Invoke("KPIs", [new DashboardKpiItem("Users", "100", UiTone.Info, "+5%") ]); + var htmlResult = Assert.IsType(result); using var writer = new StringWriter(); - result.Content.WriteTo(writer, System.Text.Encodings.Web.HtmlEncoder.Default); + htmlResult.EncodedContent.WriteTo(writer, HtmlEncoder.Default); var html = writer.ToString(); Assert.Contains("text-info", html); @@ -69,10 +72,11 @@ public void KpiSummary_ShouldRenderToneMappedItems() public void QuickActionsPanel_ShouldRenderButtons() { var component = new Cd311QuickActionsPanelViewComponent(new TablerSemanticClassMapper()); - var result = Assert.IsType(component.Invoke("Actions", [new DashboardQuickAction(new("Create"), UiTone.Accent)])); + var result = component.Invoke("Actions", [new DashboardQuickAction(new("Create"), UiTone.Accent)]); + var htmlResult = Assert.IsType(result); using var writer = new StringWriter(); - result.Content.WriteTo(writer, System.Text.Encodings.Web.HtmlEncoder.Default); + htmlResult.EncodedContent.WriteTo(writer, HtmlEncoder.Default); var html = writer.ToString(); Assert.Contains("btn-primary", html); @@ -83,10 +87,11 @@ public void QuickActionsPanel_ShouldRenderButtons() public void ActivityPanel_ShouldRenderFeedItems() { var component = new Cd311ActivityPanelViewComponent(new TablerSemanticClassMapper()); - var result = Assert.IsType(component.Invoke("Activity", [new DashboardActivityItem("Imported orders", DateTimeOffset.UtcNow, UiTone.Warning)])); + var result = component.Invoke("Activity", [new DashboardActivityItem("Imported orders", DateTimeOffset.UtcNow, UiTone.Warning)]); + var htmlResult = Assert.IsType(result); using var writer = new StringWriter(); - result.Content.WriteTo(writer, System.Text.Encodings.Web.HtmlEncoder.Default); + htmlResult.EncodedContent.WriteTo(writer, HtmlEncoder.Default); var html = writer.ToString(); Assert.Contains("Imported orders", html);